Wider ranges, an agent guide, patch and effects commands
Every effect's sliders and clamps now reach far (a nudge from a tremor to an earthquake, a banner that stays minutes). AGENTS.md documents the file schema, every option and range, the CLI, verification and worked scenarios for scripts and coding agents; attention-required effects prints the catalog as JSON, patch merges keys into a rule, docs prints the guide.
This commit is contained in:
@@ -0,0 +1,182 @@
|
|||||||
|
# Attention Required: guide for agents
|
||||||
|
|
||||||
|
This file is for an AI agent (or a script) asked to change how this machine
|
||||||
|
reacts to notifications, for example "shake the screen and play a sound when
|
||||||
|
the build fails", "let only messages from my boss through while I am in a
|
||||||
|
meeting", or "make the deliveroo nudge stronger". Everything below is exact;
|
||||||
|
nothing needs a GUI.
|
||||||
|
|
||||||
|
## Where things are
|
||||||
|
|
||||||
|
| What | Where |
|
||||||
|
|---|---|
|
||||||
|
| The configuration, the only thing to edit | `~/.config/attention-required/rules.json` |
|
||||||
|
| Command line | `attention-required` (on PATH after `./install.sh`, else `~/.config/omarchy/plugins/alanfortlink.attention-required/bin/attention-required`) |
|
||||||
|
| User-written effects | `~/.config/attention-required/effects/<name>` (executable) |
|
||||||
|
| Machine-readable effect catalog with ranges | `attention-required effects` (JSON) |
|
||||||
|
| Paused flag | `~/.local/state/attention-required/paused` (exists = paused) |
|
||||||
|
|
||||||
|
The file is watched: a saved change is live within a second, no restart. Run
|
||||||
|
`attention-required status` afterwards and check `configError` is empty.
|
||||||
|
|
||||||
|
## The rules file
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"whileDnd": true,
|
||||||
|
"letThrough": true,
|
||||||
|
"defaults": { "nudge": { "intensity": 6 } },
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"name": "deliveries",
|
||||||
|
"enabled": true,
|
||||||
|
"words": ["deliveroo", "/order #\\d+/"],
|
||||||
|
"match": "any",
|
||||||
|
"fields": ["summary", "body"],
|
||||||
|
"apps": ["Google Chrome"],
|
||||||
|
"effects": ["nudge", { "type": "banner", "duration": 6, "position": "center" }],
|
||||||
|
"cooldown": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Top level:
|
||||||
|
|
||||||
|
| Key | Type | Default | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `version` | 1 | required | Only 1 exists. |
|
||||||
|
| `whileDnd` | bool | true | Rules still fire while notifications are silenced (Do Not Disturb). |
|
||||||
|
| `letThrough` | bool | true | A notification a rule matched while silenced is posted again so its toast shows. |
|
||||||
|
| `defaults` | object | {} | Per effect type, options applied to every rule that does not set them: `{"nudge": {"speed": 15}}`. |
|
||||||
|
| `rules` | array | [] | Evaluated in order; every matching rule fires (no first-match stop). |
|
||||||
|
|
||||||
|
A rule:
|
||||||
|
|
||||||
|
| Key | Type | Default | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `name` | string | `rule-N` | Unique; used by the CLI and in logs. |
|
||||||
|
| `enabled` | bool | true | `false` keeps the rule without using it. |
|
||||||
|
| `words` | string[] | [] | Case-insensitive substrings looked for in the notification. A string of the form `/pattern/flags` is a JavaScript regular expression (flags default to `i`). **Empty means every notification.** |
|
||||||
|
| `match` | `"any"` \| `"all"` | any | One word is enough, or every word must be present. |
|
||||||
|
| `fields` | string[] | ["summary","body"] | Where words are looked for: `summary` (title), `body` (markup stripped), `app`. |
|
||||||
|
| `apps` | string[] | [] | Case-insensitive substrings of the sending app's name as it arrives on D-Bus (`"chrome"` matches `Google Chrome`). **Empty means any app.** Web apps in Chromium browsers send under the browser's name. `attention-required status` lists names actually seen (`topApps`). |
|
||||||
|
| `effects` | (string \| object)[] | `["nudge"]` if the key is absent | What runs, in order. A string is an effect with default options; an object is `{"type": ..., option: value, ...}`. An explicit `[]` matches but does nothing. |
|
||||||
|
| `cooldown` | number ≥ 0 | 3 | Seconds during which the rule will not fire again. |
|
||||||
|
|
||||||
|
Matching: the rule must pass `apps` (if any) and `words` (if any). A rule with
|
||||||
|
neither matches every notification. Silenced notifications are matched too
|
||||||
|
unless `whileDnd` is false. A notification updated in place by its sender
|
||||||
|
(same replaces-id) is re-evaluated but a rule fires once per notification.
|
||||||
|
|
||||||
|
## Effects
|
||||||
|
|
||||||
|
`attention-required effects` prints the catalog as JSON: for every effect its
|
||||||
|
`type`, `rows` (numeric options: `key`, `min`, `max`, `step`, `fallback`,
|
||||||
|
`unit`) and `options` (`type: "enum"` with `values`, or `type: "text"`).
|
||||||
|
Values outside a range are clamped when the effect runs. Every effect has
|
||||||
|
`duration`, `intensity` and `speed` where they make sense. Summary:
|
||||||
|
|
||||||
|
| type | duration | intensity | speed | other options |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `nudge` | seconds, 0.1..30 (1) | how far the picture moves, 0.1..100 (1.5) | new positions per second, 1..1000 (200) | |
|
||||||
|
| `flash` | seconds, 0.1..60 (1) | glow opacity 0.01..1 (0.9) | pulses per second 0.1..30 (3) | `thickness` px 1..2000 (64); `color`: `accent`, `urgent`, `foreground` or any CSS color |
|
||||||
|
| `banner` | seconds it stays 0.1..300 (3) | size 0.2..6 (1) | slide speed 0.2..50 (4) | `position`: `top`, `center`, `bottom`; `color`; `text` template |
|
||||||
|
| `airplane` | flight seconds 0.5..120 (7) | size 0.2..10 (1) | | `altitude` 0..1 from the top (0.2); `direction`: `ltr`, `rtl`; `text` template |
|
||||||
|
| `confetti` | seconds it keeps coming 0.1..60 (1) | amount 0.05..20 (1) | launch power 0.1..10 (1) | `style`: `cannons` (bottom corners, up), `burst` (centre), `rain` (top) |
|
||||||
|
| `blink` | seconds 0.1..30 (1) | darkness 0.01..1 (0.6) | blinks per second 0.2..30 (2) | |
|
||||||
|
| `sound` | | volume 0..2 (1) | times played 1..50 (1) | `file`: path to a sound file (default: freedesktop's new-message chime) |
|
||||||
|
| `focus` | | | | `window`: a window class or title to focus instead of the sending app |
|
||||||
|
| `command` | | | | `run`: a shell command, run as the user with `AR_APP`, `AR_SUMMARY`, `AR_BODY`, `AR_RULE`, `AR_KEY`, `AR_URGENCY` and every effect option as `AR_OPT_<NAME>` in the environment |
|
||||||
|
|
||||||
|
Text templates (`banner`, `airplane`) take `{summary}`, `{body}`, `{app}`,
|
||||||
|
`{rule}`; empty means the summary (the banner also shows the body then).
|
||||||
|
|
||||||
|
Custom effects: an executable at `~/.config/attention-required/effects/<name>`
|
||||||
|
is used for effect type `<name>`, with the same environment as `command`.
|
||||||
|
Options given on the effect object arrive as `AR_OPT_<KEY>` (upper-cased).
|
||||||
|
|
||||||
|
## Changing things
|
||||||
|
|
||||||
|
Prefer the CLI for single changes; edit the file for anything larger.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
attention-required list # table of rules
|
||||||
|
attention-required export # the file, to stdout
|
||||||
|
attention-required add NAME --words a,b --apps Slack --effects nudge,flash --cooldown 10 [--all]
|
||||||
|
attention-required patch NAME '{"effects":[{"type":"banner","duration":10}],"cooldown":0}' # merge keys into a rule
|
||||||
|
attention-required remove NAME | enable NAME | disable NAME
|
||||||
|
attention-required set nudge intensity 6 # top-level defaults for an effect
|
||||||
|
attention-required import FILE # replace the whole file (old one kept as .bak)
|
||||||
|
attention-required toggle | on | off # pause or resume every effect
|
||||||
|
```
|
||||||
|
|
||||||
|
Editing the file directly: read it, change it, write it back whole and valid.
|
||||||
|
Keep keys you do not understand; the popup and other tools may have added
|
||||||
|
them. `jq` is the safe way:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
f=~/.config/attention-required/rules.json
|
||||||
|
jq '.rules += [{"name":"build","words":["build failed"],"apps":["Ghostty"],"effects":[{"type":"flash","color":"urgent"},"sound"]}]' "$f" > "$f.new" && mv "$f.new" "$f"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Checking the result
|
||||||
|
|
||||||
|
```bash
|
||||||
|
attention-required status # configError, rules count, what fired last, app names seen
|
||||||
|
attention-required simulate "Slack" "boss: are you there?" "need the numbers asap" # runs a made-up notification through the rules, effects included
|
||||||
|
attention-required test '{"type":"nudge","intensity":6,"speed":15}' # runs one effect with these options
|
||||||
|
notify-send -a "Slack" "boss" "asap" # a real notification (with Do Not Disturb on, use -a: a bare notify-send is dropped by the desktop)
|
||||||
|
```
|
||||||
|
|
||||||
|
`simulate` returns `matched: <rule names>` or `no rule matched`.
|
||||||
|
|
||||||
|
## Scenarios
|
||||||
|
|
||||||
|
**Build failures get a red flash and a chime, everything else stays quiet**
|
||||||
|
```bash
|
||||||
|
attention-required add build --words "build failed,tests failed,error:" --apps Ghostty --effects flash,sound
|
||||||
|
attention-required patch build '{"effects":[{"type":"flash","color":"urgent","duration":2},"sound"],"cooldown":10}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Meeting mode: silence everything, let only the boss on Slack through**
|
||||||
|
```bash
|
||||||
|
omarchy toggle notification silencing # Do Not Disturb on (the bell in the bar does the same)
|
||||||
|
attention-required add boss --words "Alice" --apps Slack --effects banner,focus
|
||||||
|
```
|
||||||
|
With `whileDnd` and `letThrough` on (the defaults), Alice's messages fire the
|
||||||
|
rule, are shown as a toast, and everything else stays hidden.
|
||||||
|
|
||||||
|
**Deliveries: nudge harder, show a big card, bring the browser forward**
|
||||||
|
```bash
|
||||||
|
attention-required patch deliveries '{"words":["deliveroo","rider","your order"],"apps":["Google Chrome"],"effects":[{"type":"nudge","intensity":6,"speed":15,"duration":1.5},{"type":"banner","position":"center","duration":8},"focus"]}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Say it out loud with your own effect**
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.config/attention-required/effects
|
||||||
|
cat > ~/.config/attention-required/effects/speak <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
exec spd-say -- "$AR_SUMMARY"
|
||||||
|
EOF
|
||||||
|
chmod +x ~/.config/attention-required/effects/speak
|
||||||
|
attention-required patch boss '{"effects":["banner","speak"]}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pause during a screen share, resume after**
|
||||||
|
```bash
|
||||||
|
attention-required off
|
||||||
|
attention-required on
|
||||||
|
```
|
||||||
|
|
||||||
|
## Limits worth knowing
|
||||||
|
|
||||||
|
- Words are matched against the notification's title and body only (add
|
||||||
|
`"app"` to `fields` to match the app name too). Bodies have markup stripped.
|
||||||
|
- The nudge changes two Hyprland options for the duration of the shake and
|
||||||
|
restores them. It needs the Lua-configured Hyprland that Omarchy 4 ships.
|
||||||
|
- Notification fields are clipped (summary 2000, body 8000 characters) before
|
||||||
|
matching; bus messages over 256 KB are ignored.
|
||||||
|
- A `command` effect runs whatever `run` says. Do not write one from untrusted
|
||||||
|
input.
|
||||||
+3
-3
@@ -38,9 +38,9 @@ Item {
|
|||||||
opts = opts || {}
|
opts = opts || {}
|
||||||
text = Rules.renderTemplate(opts.text, notif, rule)
|
text = Rules.renderTemplate(opts.text, notif, rule)
|
||||||
direction = opts.direction === "rtl" ? "rtl" : "ltr"
|
direction = opts.direction === "rtl" ? "rtl" : "ltr"
|
||||||
size = number(opts.intensity, 1, 0.5, 3)
|
size = number(opts.intensity, 1, 0.2, 10)
|
||||||
altitude = number(opts.altitude, 0.2, 0.05, 0.95)
|
altitude = number(opts.altitude, 0.2, 0, 1)
|
||||||
flightMs = Math.round(number(opts.duration, 7, 2, 20) * 1000)
|
flightMs = Math.round(number(opts.duration, 7, 0.5, 120) * 1000)
|
||||||
anim.stop()
|
anim.stop()
|
||||||
flight = 0
|
flight = 0
|
||||||
active = true
|
active = true
|
||||||
|
|||||||
+3
-3
@@ -47,9 +47,9 @@ Item {
|
|||||||
title = Rules.renderTemplate(custom, notif, rule)
|
title = Rules.renderTemplate(custom, notif, rule)
|
||||||
body = custom ? "" : Rules.stripTags(notif ? notif.body : "")
|
body = custom ? "" : Rules.stripTags(notif ? notif.body : "")
|
||||||
position = opts.position === "center" || opts.position === "bottom" ? String(opts.position) : "top"
|
position = opts.position === "center" || opts.position === "bottom" ? String(opts.position) : "top"
|
||||||
size = number(opts.intensity, 1, 0.5, 2.5)
|
size = number(opts.intensity, 1, 0.2, 6)
|
||||||
slideMs = Math.round(1000 / number(opts.speed, 4, 1, 10))
|
slideMs = Math.round(1000 / number(opts.speed, 4, 0.2, 50))
|
||||||
holdMs = Math.round(number(opts.duration, 3, 0.5, 30) * 1000)
|
holdMs = Math.round(number(opts.duration, 3, 0.1, 300) * 1000)
|
||||||
glow = resolveColor(opts.color)
|
glow = resolveColor(opts.color)
|
||||||
off.stop()
|
off.stop()
|
||||||
hide.stop()
|
hide.stop()
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ Item {
|
|||||||
|
|
||||||
function trigger(opts, notif, rule) {
|
function trigger(opts, notif, rule) {
|
||||||
opts = opts || {}
|
opts = opts || {}
|
||||||
var duration = number(opts.duration, 1, 0.2, 5)
|
var duration = number(opts.duration, 1, 0.1, 30)
|
||||||
var speed = number(opts.speed, 2, 1, 10)
|
var speed = number(opts.speed, 2, 0.2, 30)
|
||||||
darkness = number(opts.intensity, 0.6, 0.1, 1)
|
darkness = number(opts.intensity, 0.6, 0.01, 1)
|
||||||
pulseMs = Math.round(1000 / speed)
|
pulseMs = Math.round(1000 / speed)
|
||||||
anim.stop()
|
anim.stop()
|
||||||
level = 0
|
level = 0
|
||||||
|
|||||||
+3
-3
@@ -31,10 +31,10 @@ Item {
|
|||||||
|
|
||||||
function trigger(opts, notif, rule) {
|
function trigger(opts, notif, rule) {
|
||||||
opts = opts || {}
|
opts = opts || {}
|
||||||
amount = number(opts.intensity, 1, 0.2, 3)
|
amount = number(opts.intensity, 1, 0.05, 20)
|
||||||
power = number(opts.speed, 1, 0.3, 3)
|
power = number(opts.speed, 1, 0.1, 10)
|
||||||
style = opts.style === "rain" || opts.style === "burst" ? String(opts.style) : "cannons"
|
style = opts.style === "rain" || opts.style === "burst" ? String(opts.style) : "cannons"
|
||||||
var seconds = number(opts.duration, 1, 0.5, 15)
|
var seconds = number(opts.duration, 1, 0.1, 60)
|
||||||
active = true
|
active = true
|
||||||
emitting = true
|
emitting = true
|
||||||
stopEmit.interval = Math.round(seconds * 1000)
|
stopEmit.interval = Math.round(seconds * 1000)
|
||||||
|
|||||||
+21
-21
@@ -10,9 +10,9 @@ var EFFECTS = [
|
|||||||
type: "nudge", label: "Nudge", icon: "",
|
type: "nudge", label: "Nudge", icon: "",
|
||||||
subtitle: "Shakes the screen, like a phone buzzing",
|
subtitle: "Shakes the screen, like a phone buzzing",
|
||||||
rows: [
|
rows: [
|
||||||
{ key: "duration", label: "Duration", min: 0.2, max: 5, step: 0.1, fallback: 1, unit: " s" },
|
{ key: "duration", label: "Duration", min: 0.1, max: 30, step: 0.1, fallback: 1, unit: " s" },
|
||||||
{ key: "intensity", label: "Intensity", min: 0.5, max: 30, step: 0.5, fallback: 1.5, unit: "" },
|
{ key: "intensity", label: "Intensity", min: 0.1, max: 100, step: 0.1, fallback: 1.5, unit: "" },
|
||||||
{ key: "speed", label: "Speed", min: 10, max: 400, step: 10, fallback: 200, unit: " /s" }
|
{ key: "speed", label: "Speed", min: 1, max: 1000, step: 1, fallback: 200, unit: " /s" }
|
||||||
],
|
],
|
||||||
options: []
|
options: []
|
||||||
},
|
},
|
||||||
@@ -20,10 +20,10 @@ var EFFECTS = [
|
|||||||
type: "flash", label: "Flash", icon: "",
|
type: "flash", label: "Flash", icon: "",
|
||||||
subtitle: "A glow pulses in from the edges of the screen",
|
subtitle: "A glow pulses in from the edges of the screen",
|
||||||
rows: [
|
rows: [
|
||||||
{ key: "duration", label: "Duration", min: 0.2, max: 10, step: 0.1, fallback: 1, unit: " s" },
|
{ key: "duration", label: "Duration", min: 0.1, max: 60, step: 0.1, fallback: 1, unit: " s" },
|
||||||
{ key: "intensity", label: "Intensity", min: 0.05, max: 1, step: 0.05, fallback: 0.9, unit: "" },
|
{ key: "intensity", label: "Intensity", min: 0.01, max: 1, step: 0.01, fallback: 0.9, unit: "" },
|
||||||
{ key: "speed", label: "Pulses", min: 0.5, max: 12, step: 0.5, fallback: 3, unit: " /s" },
|
{ key: "speed", label: "Pulses", min: 0.1, max: 30, step: 0.1, fallback: 3, unit: " /s" },
|
||||||
{ key: "thickness", label: "Thickness", min: 8, max: 400, step: 8, fallback: 64, unit: " px" }
|
{ key: "thickness", label: "Thickness", min: 1, max: 2000, step: 1, fallback: 64, unit: " px" }
|
||||||
],
|
],
|
||||||
options: [
|
options: [
|
||||||
{ key: "color", label: "Color", type: "enum", fallback: "accent",
|
{ key: "color", label: "Color", type: "enum", fallback: "accent",
|
||||||
@@ -34,9 +34,9 @@ var EFFECTS = [
|
|||||||
type: "banner", label: "Banner", icon: "",
|
type: "banner", label: "Banner", icon: "",
|
||||||
subtitle: "The message drops in as a big card",
|
subtitle: "The message drops in as a big card",
|
||||||
rows: [
|
rows: [
|
||||||
{ key: "duration", label: "Stays for", min: 0.5, max: 30, step: 0.5, fallback: 3, unit: " s" },
|
{ key: "duration", label: "Stays for", min: 0.1, max: 300, step: 0.1, fallback: 3, unit: " s" },
|
||||||
{ key: "intensity", label: "Size", min: 0.5, max: 2.5, step: 0.1, fallback: 1, unit: "×" },
|
{ key: "intensity", label: "Size", min: 0.2, max: 6, step: 0.1, fallback: 1, unit: "×" },
|
||||||
{ key: "speed", label: "Slide", min: 1, max: 10, step: 0.5, fallback: 4, unit: " /s" }
|
{ key: "speed", label: "Slide", min: 0.2, max: 50, step: 0.1, fallback: 4, unit: " /s" }
|
||||||
],
|
],
|
||||||
options: [
|
options: [
|
||||||
{ key: "position", label: "Position", type: "enum", fallback: "top",
|
{ key: "position", label: "Position", type: "enum", fallback: "top",
|
||||||
@@ -50,9 +50,9 @@ var EFFECTS = [
|
|||||||
type: "airplane", label: "Airplane", icon: "",
|
type: "airplane", label: "Airplane", icon: "",
|
||||||
subtitle: "A plane tows the message across the screen",
|
subtitle: "A plane tows the message across the screen",
|
||||||
rows: [
|
rows: [
|
||||||
{ key: "duration", label: "Flight", min: 2, max: 20, step: 0.5, fallback: 7, unit: " s" },
|
{ key: "duration", label: "Flight", min: 0.5, max: 120, step: 0.1, fallback: 7, unit: " s" },
|
||||||
{ key: "intensity", label: "Size", min: 0.5, max: 3, step: 0.1, fallback: 1, unit: "×" },
|
{ key: "intensity", label: "Size", min: 0.2, max: 10, step: 0.1, fallback: 1, unit: "×" },
|
||||||
{ key: "altitude", label: "Altitude", min: 0.05, max: 0.95, step: 0.05, fallback: 0.2, unit: "" }
|
{ key: "altitude", label: "Altitude", min: 0, max: 1, step: 0.01, fallback: 0.2, unit: "" }
|
||||||
],
|
],
|
||||||
options: [
|
options: [
|
||||||
{ key: "direction", label: "Direction", type: "enum", fallback: "ltr",
|
{ key: "direction", label: "Direction", type: "enum", fallback: "ltr",
|
||||||
@@ -64,9 +64,9 @@ var EFFECTS = [
|
|||||||
type: "confetti", label: "Confetti", icon: "",
|
type: "confetti", label: "Confetti", icon: "",
|
||||||
subtitle: "Confetti pops up across the screen",
|
subtitle: "Confetti pops up across the screen",
|
||||||
rows: [
|
rows: [
|
||||||
{ key: "duration", label: "Duration", min: 0.5, max: 15, step: 0.5, fallback: 1, unit: " s" },
|
{ key: "duration", label: "Duration", min: 0.1, max: 60, step: 0.1, fallback: 1, unit: " s" },
|
||||||
{ key: "intensity", label: "Amount", min: 0.2, max: 3, step: 0.1, fallback: 1, unit: "×" },
|
{ key: "intensity", label: "Amount", min: 0.05, max: 20, step: 0.05, fallback: 1, unit: "×" },
|
||||||
{ key: "speed", label: "Power", min: 0.3, max: 3, step: 0.1, fallback: 1, unit: "×" }
|
{ key: "speed", label: "Power", min: 0.1, max: 10, step: 0.1, fallback: 1, unit: "×" }
|
||||||
],
|
],
|
||||||
options: [
|
options: [
|
||||||
{ key: "style", label: "Comes from", type: "enum", fallback: "cannons",
|
{ key: "style", label: "Comes from", type: "enum", fallback: "cannons",
|
||||||
@@ -77,9 +77,9 @@ var EFFECTS = [
|
|||||||
type: "blink", label: "Blink", icon: "",
|
type: "blink", label: "Blink", icon: "",
|
||||||
subtitle: "The screen dims and comes back",
|
subtitle: "The screen dims and comes back",
|
||||||
rows: [
|
rows: [
|
||||||
{ key: "duration", label: "Duration", min: 0.2, max: 5, step: 0.1, fallback: 1, unit: " s" },
|
{ key: "duration", label: "Duration", min: 0.1, max: 30, step: 0.1, fallback: 1, unit: " s" },
|
||||||
{ key: "intensity", label: "Darkness", min: 0.1, max: 1, step: 0.05, fallback: 0.6, unit: "" },
|
{ key: "intensity", label: "Darkness", min: 0.01, max: 1, step: 0.01, fallback: 0.6, unit: "" },
|
||||||
{ key: "speed", label: "Blinks", min: 1, max: 10, step: 0.5, fallback: 2, unit: " /s" }
|
{ key: "speed", label: "Blinks", min: 0.2, max: 30, step: 0.1, fallback: 2, unit: " /s" }
|
||||||
],
|
],
|
||||||
options: []
|
options: []
|
||||||
},
|
},
|
||||||
@@ -87,8 +87,8 @@ var EFFECTS = [
|
|||||||
type: "sound", label: "Sound", icon: "",
|
type: "sound", label: "Sound", icon: "",
|
||||||
subtitle: "Plays a chime",
|
subtitle: "Plays a chime",
|
||||||
rows: [
|
rows: [
|
||||||
{ key: "intensity", label: "Volume", min: 0, max: 1, step: 0.05, fallback: 1, unit: "" },
|
{ key: "intensity", label: "Volume", min: 0, max: 2, step: 0.01, fallback: 1, unit: "" },
|
||||||
{ key: "speed", label: "Repeat", min: 1, max: 5, step: 1, fallback: 1, unit: " ×" }
|
{ key: "speed", label: "Repeat", min: 1, max: 50, step: 1, fallback: 1, unit: " ×" }
|
||||||
],
|
],
|
||||||
options: [
|
options: [
|
||||||
{ key: "file", label: "File", type: "text", fallback: "", placeholder: "empty: the default chime · or a path to a sound file" }
|
{ key: "file", label: "File", type: "text", fallback: "", placeholder: "empty: the default chime · or a path to a sound file" }
|
||||||
|
|||||||
@@ -45,10 +45,10 @@ Item {
|
|||||||
function trigger(opts) {
|
function trigger(opts) {
|
||||||
opts = opts || {}
|
opts = opts || {}
|
||||||
glow = resolveColor(opts.color)
|
glow = resolveColor(opts.color)
|
||||||
var duration = number(opts.duration, 1, 0.1, 30)
|
var duration = number(opts.duration, 1, 0.1, 60)
|
||||||
var speed = number(opts.speed, 3, 0.2, 20)
|
var speed = number(opts.speed, 3, 0.1, 30)
|
||||||
intensity = number(opts.intensity, 0.9, 0.05, 1)
|
intensity = number(opts.intensity, 0.9, 0.01, 1)
|
||||||
thickness = Math.round(number(opts.thickness, 64, 4, 600))
|
thickness = Math.round(number(opts.thickness, 64, 1, 2000))
|
||||||
pulseMs = Math.round(1000 / speed)
|
pulseMs = Math.round(1000 / speed)
|
||||||
pulses = Math.max(1, Math.round(duration * speed))
|
pulses = Math.max(1, Math.round(duration * speed))
|
||||||
anim.stop()
|
anim.stop()
|
||||||
|
|||||||
@@ -94,6 +94,17 @@ that does not set the option itself.
|
|||||||
`color` is `accent`, `urgent`, `foreground` or any CSS color. Text templates
|
`color` is `accent`, `urgent`, `foreground` or any CSS color. Text templates
|
||||||
take `{summary}`, `{body}`, `{app}`, `{rule}`. Your own effect is an executable
|
take `{summary}`, `{body}`, `{app}`, `{rule}`. Your own effect is an executable
|
||||||
in `~/.config/attention-required/effects/<name>` with the same environment.
|
in `~/.config/attention-required/effects/<name>` with the same environment.
|
||||||
|
The sliders go far: a nudge can be a tremor or an earthquake, a banner can
|
||||||
|
stay for five minutes. `attention-required effects` prints every option and
|
||||||
|
range as JSON.
|
||||||
|
|
||||||
|
## For agents and scripts
|
||||||
|
|
||||||
|
[AGENTS.md](AGENTS.md) is the complete, exact reference: the file schema, every
|
||||||
|
effect option with its range, the CLI, how to verify, and worked scenarios.
|
||||||
|
Point your coding agent at it (`attention-required docs` prints it) and ask
|
||||||
|
for "a red flash and a chime when the build fails" or "meeting mode: only my
|
||||||
|
boss on Slack gets through".
|
||||||
|
|
||||||
## Command line
|
## Command line
|
||||||
|
|
||||||
@@ -102,7 +113,8 @@ attention-required list | add NAME --words a,b --apps x --effects nudge,flash |
|
|||||||
attention-required enable NAME | disable NAME | toggle | on | off | settings
|
attention-required enable NAME | disable NAME | toggle | on | off | settings
|
||||||
attention-required test banner | test '{"type":"nudge","intensity":6,"speed":15}'
|
attention-required test banner | test '{"type":"nudge","intensity":6,"speed":15}'
|
||||||
attention-required simulate "Google Chrome" "Your rider has arrived" "deliveroo.co.uk"
|
attention-required simulate "Google Chrome" "Your rider has arrived" "deliveroo.co.uk"
|
||||||
attention-required set nudge intensity 6 | status | export [FILE] | import FILE | edit
|
attention-required patch NAME '{"cooldown":0,"effects":[{"type":"banner","duration":10}]}'
|
||||||
|
attention-required set nudge intensity 6 | status | effects | export [FILE] | import FILE | edit
|
||||||
./demo.sh # a narrated tour of every effect, driven by real notifications
|
./demo.sh # a narrated tour of every effect, driven by real notifications
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import QtQuick
|
|||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import "Rules.js" as Rules
|
import "Rules.js" as Rules
|
||||||
|
import "EffectCatalog.js" as Catalog
|
||||||
|
|
||||||
// Headless service: streams every notification the shell puts on screen,
|
// Headless service: streams every notification the shell puts on screen,
|
||||||
// runs it past the rules in ~/.config/attention-required/rules.json, and
|
// runs it past the rules in ~/.config/attention-required/rules.json, and
|
||||||
@@ -602,6 +603,11 @@ Item {
|
|||||||
return "reloading " + root.rulesPath
|
return "reloading " + root.rulesPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The effect catalog with every option and its range, for scripts and agents.
|
||||||
|
function effects(): string {
|
||||||
|
return JSON.stringify(Catalog.EFFECTS)
|
||||||
|
}
|
||||||
|
|
||||||
// attention-required toggle | on | off
|
// attention-required toggle | on | off
|
||||||
function toggle(): string {
|
function toggle(): string {
|
||||||
root.setEnabled(!root.enabled)
|
root.setEnabled(!root.enabled)
|
||||||
|
|||||||
@@ -9,8 +9,11 @@
|
|||||||
# --effects e1,e2 nudge | flash | sound | command | your own (default: nudge)
|
# --effects e1,e2 nudge | flash | sound | command | your own (default: nudge)
|
||||||
# --all every word must be present, not any
|
# --all every word must be present, not any
|
||||||
# --cooldown SECONDS at most one firing per rule in this many seconds (default 3)
|
# --cooldown SECONDS at most one firing per rule in this many seconds (default 3)
|
||||||
|
# attention-required patch NAME '{"key": value, ...}' merge these keys into a rule (a null value removes the key)
|
||||||
# attention-required remove NAME
|
# attention-required remove NAME
|
||||||
# attention-required enable NAME | disable NAME
|
# attention-required enable NAME | disable NAME
|
||||||
|
# attention-required effects the effect catalog with every option and range, as JSON
|
||||||
|
# attention-required docs print the guide for scripts and agents (AGENTS.md)
|
||||||
# attention-required test EFFECT run one effect now, e.g. `test nudge`
|
# attention-required test EFFECT run one effect now, e.g. `test nudge`
|
||||||
# or `test '{"type":"flash","color":"urgent"}'`
|
# or `test '{"type":"flash","color":"urgent"}'`
|
||||||
# attention-required simulate APP SUMMARY [BODY] run a made-up notification past the rules
|
# attention-required simulate APP SUMMARY [BODY] run a made-up notification past the rules
|
||||||
@@ -108,6 +111,18 @@ cmd_add() {
|
|||||||
ipc reload >/dev/null 2>&1 || true
|
ipc reload >/dev/null 2>&1 || true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd_patch() {
|
||||||
|
local name=${1:-} patch=${2:-}
|
||||||
|
[[ -n $name && -n $patch ]] || die "usage: patch NAME '{\"key\": value, ...}'"
|
||||||
|
jq -e 'type == "object"' <<<"$patch" >/dev/null 2>&1 || die "the patch must be a JSON object"
|
||||||
|
ensure_rules
|
||||||
|
have_rule "$name" || die "no rule named '$name'"
|
||||||
|
jq --arg n "$name" --argjson p "$patch" '
|
||||||
|
.rules = ((.rules // []) | map(if .name == $n then (. + $p | with_entries(select(.value != null))) else . end))' "$rules" | write_rules
|
||||||
|
echo "patched '$name'"
|
||||||
|
ipc reload >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
cmd_remove() {
|
cmd_remove() {
|
||||||
local name=${1:-}
|
local name=${1:-}
|
||||||
[[ -n $name ]] || die "usage: remove NAME"
|
[[ -n $name ]] || die "usage: remove NAME"
|
||||||
@@ -131,7 +146,10 @@ cmd_toggle() {
|
|||||||
case ${1:-help} in
|
case ${1:-help} in
|
||||||
list|ls) cmd_list ;;
|
list|ls) cmd_list ;;
|
||||||
add) shift; cmd_add "$@" ;;
|
add) shift; cmd_add "$@" ;;
|
||||||
|
patch) shift; cmd_patch "$@" ;;
|
||||||
remove|rm) shift; cmd_remove "$@" ;;
|
remove|rm) shift; cmd_remove "$@" ;;
|
||||||
|
effects) ipc effects | jq . ;;
|
||||||
|
docs) cat "$here/AGENTS.md" ;;
|
||||||
enable) shift; cmd_toggle true "$@" ;;
|
enable) shift; cmd_toggle true "$@" ;;
|
||||||
disable) shift; cmd_toggle false "$@" ;;
|
disable) shift; cmd_toggle false "$@" ;;
|
||||||
test) [[ -n ${2:-} ]] || die "usage: test EFFECT"; ipc test "$2" ;;
|
test) [[ -n ${2:-} ]] || die "usage: test EFFECT"; ipc test "$2" ;;
|
||||||
|
|||||||
+2
-2
@@ -11,7 +11,7 @@
|
|||||||
#
|
#
|
||||||
# Options (set on the effect in rules.json):
|
# Options (set on the effect in rules.json):
|
||||||
# duration seconds, default 1
|
# duration seconds, default 1
|
||||||
# intensity 0.5..30, how far the picture moves, default 1.5 (a phone buzz; 6 is a proper MSN nudge)
|
# intensity 0.1..100, how far the picture moves, default 1.5 (a phone buzz; 6 is a proper MSN nudge)
|
||||||
# speed new positions per second, default 200: at or above the refresh rate every frame differs
|
# speed new positions per second, default 200: at or above the refresh rate every frame differs
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ shader=$run_dir/nudge.frag
|
|||||||
exec 9>"$run_dir/nudge.lock"
|
exec 9>"$run_dir/nudge.lock"
|
||||||
flock -n 9 || exit 0
|
flock -n 9 || exit 0
|
||||||
|
|
||||||
amp=$(awk -v i="$intensity" 'BEGIN { printf "%.5f", (i > 30 ? 30 : i) / 1000 }')
|
amp=$(awk -v i="$intensity" 'BEGIN { printf "%.5f", (i > 100 ? 100 : i) / 1000 }')
|
||||||
rate=$(awk -v s="$speed" 'BEGIN { printf "%.1f", (s < 1 ? 1 : s) }')
|
rate=$(awk -v s="$speed" 'BEGIN { printf "%.1f", (s < 1 ? 1 : s) }')
|
||||||
|
|
||||||
cat > "$shader" <<FRAG
|
cat > "$shader" <<FRAG
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ volume=${AR_OPT_INTENSITY:-${AR_OPT_VOLUME:-1}}
|
|||||||
times=${AR_OPT_SPEED:-${AR_OPT_REPEAT:-1}}
|
times=${AR_OPT_SPEED:-${AR_OPT_REPEAT:-1}}
|
||||||
[[ $volume =~ ^[0-9]*\.?[0-9]+$ ]] || volume=1
|
[[ $volume =~ ^[0-9]*\.?[0-9]+$ ]] || volume=1
|
||||||
[[ $times =~ ^[0-9]+$ ]] || times=1
|
[[ $times =~ ^[0-9]+$ ]] || times=1
|
||||||
|
(( times > 50 )) && times=50
|
||||||
file=${file/#\~/$HOME}
|
file=${file/#\~/$HOME}
|
||||||
|
|
||||||
[[ -r $file ]] || { echo "sound: cannot read $file" >&2; exit 1; }
|
[[ -r $file ]] || { echo "sound: cannot read $file" >&2; exit 1; }
|
||||||
|
|||||||
Reference in New Issue
Block a user