Skip to main content

nobble_addon_sdk/
addon.rs

1//! What an addon is: the trait an author implements, and its vocabulary.
2//!
3//! Moved here from `nobble-core`, where its own module doc already said these
4//! types were "destined to be part of the public SDK (ADR-0010, FR-047)". They
5//! arrived without a single `use crate::` line, so the move was a lift rather
6//! than an extraction — the boundary was already where it needed to be.
7//!
8//! **Nothing here depends on any other Nobble crate, and it must stay that
9//! way.** Every dependency added here is one a third-party addon author
10//! inherits, and FR-044 says an addon must be buildable against only the public
11//! repositories.
12
13use core::fmt;
14use std::collections::BTreeMap;
15use std::sync::Arc;
16
17use serde::{Deserialize, Serialize};
18
19/// Where an addon keeps its own credentials (ADR-0013, FR-046).
20///
21/// # Why there is no `addon` parameter
22///
23/// Because there used to be, and it was a hole. `nobble-core`'s `SecretStore`
24/// reads `fn get(&self, addon: &str, key: &str)`, and every addon was handed
25/// the same live handle to it — so **any addon could read any other addon's
26/// credentials** by passing a different string. `get("spotify",
27/// "refresh_token")` from inside an unrelated addon returned the token.
28///
29/// That could not be fixed by narrowing the trait while addons shared an
30/// address space: a handle to the store *is* the capability, and asking a third
31/// party not to use one they hold is not a control. It is fixed by taking the
32/// namespace out of the caller's hands entirely. An addon says which *key* it
33/// wants and never which addon it is; whoever hands out the handle decides
34/// that, and in the daemon it is bound to the addon the handle was made for.
35///
36/// The result is a type in which the old mistake cannot be written down.
37/// [ADR-0016](../../../docs/decisions/0016-addon-process-boundary.md).
38pub trait Credentials: Send + Sync {
39    /// Read one of this addon's credentials.
40    fn get(&self, key: &str) -> Option<String>;
41
42    /// Store one.
43    ///
44    /// # Errors
45    /// If the platform store refused it.
46    fn set(&self, key: &str, value: &str) -> Result<(), String>;
47
48    /// Forget one. Absent is the outcome, so forgetting nothing is success.
49    fn clear(&self, key: &str);
50}
51
52/// A shared [`Credentials`], because an addon needs it after configuration too.
53pub type CredentialHandle = Arc<dyn Credentials>;
54
55/// Somewhere to remember things that are **not** secrets (ADR-0027).
56///
57/// The third place an addon can put something, after the settings file it is
58/// given and the credential store it reaches through [`Credentials`]. Those two
59/// are split by what the thing *is*: settings are written by the user and are
60/// explicitly shareable, credentials are the whole of an account's authority.
61/// This is for everything that is neither — a list of recently used things, a
62/// cache, an ordering somebody arranged — which previously had nowhere to go.
63///
64/// # Three things worth knowing before using it
65///
66/// **It is encrypted at rest and you cannot switch that off.** The host holds
67/// the key material and does the work; what crosses this interface is plaintext.
68/// An addon does not decide, because an addon that forgot would produce a
69/// plaintext file indistinguishable from one that had thought about it.
70///
71/// **That is protection at rest and nothing more.** The host must be able to
72/// decrypt in order to hand the value back, so anything running as the same
73/// user can obtain the same plaintext. It defends a profile directory that is
74/// copied, synced, backed up or attached to a bug report. Do not tell a user it
75/// does more.
76///
77/// **It can forget.** A store whose key material is gone reads as empty rather
78/// than as an error — see [`Self::get`]. Anything that cannot survive being
79/// forgotten belongs in the settings file or the credential store instead.
80pub trait Store: Send + Sync {
81    /// Read one, or `None`.
82    ///
83    /// `None` covers *never written*, *cleared*, and *the key material is no
84    /// longer readable*. Deliberately one answer: the last is indistinguishable
85    /// from the first without leaking whether a value once existed, and an
86    /// addon could not act differently on it anyway.
87    fn get(&self, key: &str) -> Option<String>;
88
89    /// Write one.
90    ///
91    /// # Errors
92    /// If the host refused — no implementation on this platform, or the write
93    /// failed. Storing less than you wanted is recoverable; storing it in the
94    /// clear instead is not, so there is no fallback.
95    fn set(&self, key: &str, value: &str) -> Result<(), String>;
96
97    /// Forget one. Absent is the outcome, so forgetting nothing is success.
98    fn clear(&self, key: &str);
99
100    /// Every key currently stored.
101    ///
102    /// What makes a store **clearable and inspectable** rather than a place
103    /// data accumulates unseen — an addon that keeps a record of people has to
104    /// be able to show it and empty it, and cannot do either without this.
105    fn keys(&self) -> Vec<String>;
106}
107
108/// A shared [`Store`], for the same reason [`CredentialHandle`] is shared.
109pub type StoreHandle = Arc<dyn Store>;
110
111/// Full scale for a 14-bit fader value.
112///
113/// Lives here rather than in `nobble-core` because [`Invocation::fraction`]
114/// needs it and this crate may not depend on any other. `nobble-core`
115/// re-exports it, so there is still one definition — the direction is decided
116/// by the SDK's no-dependency rule rather than by where it feels like it
117/// belongs.
118///
119/// 14-bit by ADR-0007 Amendment 1: 7 bits over a 100 mm throw is 0.78 mm per
120/// step, which is felt as stepping and heard as zipper noise.
121pub const FADER_MAX: u16 = 16_383;
122
123/// What kind of input an action expects.
124///
125/// [ADR-0012]. Declared rather than inferred, because the two are not
126/// interchangeable and binding one to the other is always a mistake: a fader
127/// bound to "next track" would skip a hundred tracks across one sweep, and a
128/// key bound to "set volume" has no position to send.
129///
130/// [ADR-0012]: ../../../docs/decisions/0012-addon-actions-carry-data.md
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub enum Trigger {
133    /// A press. The ordinary case, and the default an addon author should
134    /// reach for.
135    Momentary,
136    /// A position — a fader, `0..=16383`.
137    ///
138    /// The daemon coalesces these, so an action sees where the fader ended up
139    /// rather than every point it passed through. An action that needs the
140    /// whole gesture is a different thing and this is not it.
141    Continuous,
142}
143
144impl Trigger {
145    /// How it is spelled on a wire.
146    ///
147    /// # Why this is a method and not a `match` at each end
148    ///
149    /// Because it was three `match`es, and they had to agree without anything
150    /// making them. The addon-to-daemon encoder lived in
151    /// [`run`](crate::run), the daemon-to-interface encoder in
152    /// `nobble_rpc::AddonDto::of`, and the decoder between them was
153    /// `if a.trigger == "continuous"` in the daemon — with a TypeScript
154    /// `=== "continuous"` at the far end comparing against the result. Five
155    /// hand-written copies of two strings, on a round trip where a single
156    /// disagreement makes a fader silently behave like a key: the decoder's
157    /// `else` branch is `Momentary`, so a renamed encoding does not fail, it
158    /// degrades.
159    ///
160    /// Constitution VI is about exactly that, and the remedy it prefers —
161    /// generated bindings — is unavailable inside one language. One definition
162    /// on the type is the next thing: `nobble-core` re-exports this type, so
163    /// every crate downstream is looking at the same `impl` rather than at its
164    /// own copy of the answer.
165    ///
166    /// **Not a display name.** These are protocol tokens, lowercase because
167    /// that is what is on the wire, and a renaming here is a breaking protocol
168    /// change rather than a wording change. An interface wanting *Momentary*
169    /// with a capital M writes that itself.
170    #[must_use]
171    pub const fn as_wire(self) -> &'static str {
172        match self {
173            Self::Momentary => "momentary",
174            Self::Continuous => "continuous",
175        }
176    }
177
178    /// Read one back, or `None` if this build has no name for it.
179    ///
180    /// An `Option` rather than a default, deliberately. The daemon's decode
181    /// used to fall back to [`Self::Momentary`] for anything unrecognised,
182    /// which turns an addon built against a newer SDK into a fader that behaves
183    /// like a key with nothing reported — a Principle IV collapse. Whether to
184    /// refuse or to default is the caller's decision to state out loud; this
185    /// only declines to make it for them.
186    #[must_use]
187    pub fn from_wire(s: &str) -> Option<Self> {
188        match s {
189            "momentary" => Some(Self::Momentary),
190            "continuous" => Some(Self::Continuous),
191            _ => None,
192        }
193    }
194}
195
196/// What a parameter holds, so an interface can offer the right editor.
197///
198/// A hint, not a storage type — every parameter is stored as a string
199/// (ADR-0012). Widening this is additive; a client that does not recognise a
200/// kind falls back to a text box and stays useful.
201#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202#[non_exhaustive]
203pub enum ParamKind {
204    /// Free text.
205    Text,
206    /// An application identity, in whatever form the foreground watcher
207    /// reports. The interface can offer "the application you were just in"
208    /// rather than asking someone to type an executable name from memory.
209    App,
210}
211
212impl ParamKind {
213    /// How it is spelled on a wire. See [`Trigger::as_wire`] for why this is
214    /// here rather than at each end.
215    ///
216    /// **Exhaustive, and that is the point of putting it in this crate.** This
217    /// type is `#[non_exhaustive]`, so every match on it *outside* the SDK must
218    /// carry a wildcard — which is how `nobble_rpc::param_kind_name` came to
219    /// have a `ParamKind::Text | _ => "text"` arm defending against a variant
220    /// that cannot exist in a build that compiled. Here the wildcard is not
221    /// required, so adding a kind fails to compile at the one site that has to
222    /// decide what it is called.
223    #[must_use]
224    pub const fn as_wire(self) -> &'static str {
225        match self {
226            Self::Text => "text",
227            Self::App => "app",
228        }
229    }
230
231    /// Read one back, or `None` if this build has no name for it.
232    ///
233    /// Unlike [`Trigger::from_wire`], defaulting is the *documented* behaviour
234    /// for a caller here: this type's own note says widening it is additive and
235    /// a client that does not recognise a kind should offer a text box and stay
236    /// useful. The `Option` is still the honest return, because "I do not know
237    /// this one" and "this one is text" are different facts and only the caller
238    /// knows whether the difference matters to it.
239    #[must_use]
240    pub fn from_wire(s: &str) -> Option<Self> {
241        match s {
242            "text" => Some(Self::Text),
243            "app" => Some(Self::App),
244            _ => None,
245        }
246    }
247}
248
249/// Whether something the platform named is the application a binding meant.
250///
251/// Here rather than in each addon, because [`ParamKind::App`] is declared here
252/// and an addon author has no way to guess how to compare one. The value
253/// stored in a binding is whatever Nobble's foreground watcher reports —
254/// `exe:spotify.exe` — and what the addon is comparing it against comes from
255/// somewhere else entirely, in whatever form *that* API uses.
256///
257/// So the comparison is on the **stem**: `exe:spotify.exe` and `Spotify.exe`
258/// both reduce to `spotify`. The Windows media session reports `Spotify.exe`
259/// for a desktop install and `SpotifyAB.SpotifyMusic_…!Spotify` for the Store
260/// build, and matching either exactly would work on one machine and not the
261/// next.
262///
263/// An empty target matches **nothing**, which is the opposite of what an
264/// unconstrained match would do. Naming a target means "this one", and a
265/// blank field that matched everything would turn a typo into a key that
266/// controls whatever happens to be loudest.
267///
268/// ```
269/// # use nobble_addon_sdk::app_matches;
270/// assert!(app_matches("Spotify.exe", "exe:spotify.exe"));
271/// assert!(app_matches("SpotifyAB.SpotifyMusic_zpdnekdrzrea0!Spotify", "exe:spotify.exe"));
272/// assert!(!app_matches("chrome.exe", "exe:spotify.exe"));
273/// assert!(!app_matches("Spotify.exe", ""));
274/// ```
275#[must_use]
276pub fn app_matches(reported: &str, target: &str) -> bool {
277    let stem = target
278        .rsplit(':')
279        .next()
280        .unwrap_or(target)
281        .trim_end_matches(".exe")
282        .trim_end_matches(".EXE")
283        .to_ascii_lowercase();
284    // `Invocation::param` already treats a cleared field as absent, so reaching
285    // here with an empty target is a bug rather than a user choice.
286    !stem.is_empty() && reported.to_ascii_lowercase().contains(&stem)
287}
288
289/// What one parameter holds: a value, or several.
290///
291/// [ADR-0022]. A parameter is single-valued unless its declaration says
292/// otherwise, and both shapes live in the same map because a binding stores one
293/// map whatever its parameters declared.
294///
295/// # Why an enum rather than always a list
296///
297/// Because of what it costs on disk. `#[serde(untagged)]` renders these as
298/// `app = "spotify"` and `apps = ["discord", "game"]` in the same TOML table,
299/// so a configuration written before lists existed parses unchanged and needs
300/// no migration rung. Making every value a list would have rewritten every
301/// binding anybody has, to express something almost none of them use.
302///
303/// It is also what `006-FR-024a-i` asks for in the negative: a list "MUST be
304/// widened rather than worked around with a delimiter convention the interface
305/// cannot render". A TOML array is how TOML writes a list. There is no
306/// convention to learn, and nothing to escape.
307///
308/// [ADR-0022]: ../../../docs/decisions/0022-addon-supplied-choices.md
309#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
310#[serde(untagged)]
311pub enum ParamValue {
312    /// One value. What every parameter was before ADR-0022.
313    One(String),
314    /// Several, in the order the user arranged them.
315    Many(Vec<String>),
316}
317
318impl ParamValue {
319    /// The single value, or `None` if this holds a list.
320    ///
321    /// **Not the first element**, deliberately. An addon that declared a
322    /// single-valued parameter and receives a list has been given something it
323    /// did not ask for, and quietly using the first entry would silently ignore
324    /// the rest — the failure being a key that mutes one of the three
325    /// applications somebody named. `None` reaches the required-parameter check
326    /// in `Registry::perform` and fails loudly instead.
327    #[must_use]
328    pub fn one(&self) -> Option<&str> {
329        match self {
330            Self::One(v) if !v.is_empty() => Some(v),
331            Self::One(_) | Self::Many(_) => None,
332        }
333    }
334
335    /// Every value, whether this holds one or several.
336    ///
337    /// A single value reads as a list of one, because an addon that declared
338    /// `multiple` should not have to care how the user happened to fill it in —
339    /// and a file written before lists existed contains exactly that case.
340    /// Empty strings are dropped for the same reason [`Self::one`] rejects
341    /// them: a cleared box is not a configured value.
342    #[must_use]
343    pub fn all(&self) -> Vec<&str> {
344        match self {
345            Self::One(v) => {
346                if v.is_empty() {
347                    Vec::new()
348                } else {
349                    vec![v.as_str()]
350                }
351            }
352            Self::Many(vs) => vs
353                .iter()
354                .map(String::as_str)
355                .filter(|s| !s.is_empty())
356                .collect(),
357        }
358    }
359}
360
361impl From<&str> for ParamValue {
362    fn from(v: &str) -> Self {
363        Self::One(v.to_owned())
364    }
365}
366
367/// One option a user can pick.
368#[derive(Debug, Clone, Copy, PartialEq, Eq)]
369pub struct AddonChoice {
370    /// What gets stored in the binding.
371    pub value: &'static str,
372    /// What the user reads.
373    pub label: &'static str,
374}
375
376/// One option from a [live](AddonChoices::live) source.
377///
378/// The owned twin of [`AddonChoice`], and it has to be owned: a roster is
379/// people who happen to be in a call right now, so there is nothing to borrow
380/// from and nothing `'static` to point at.
381#[derive(Debug, Clone, PartialEq, Eq)]
382pub struct Choice {
383    /// What gets stored in the binding.
384    ///
385    /// **An identity, not a name.** Whatever survives the label changing —
386    /// a rename, a nickname, a rejoin — because this is what the binding still
387    /// holds next week.
388    pub value: String,
389    /// What the user reads while choosing.
390    pub label: String,
391    /// A second line, where the label alone is ambiguous.
392    ///
393    /// Two people called Alex in one call is ordinary, and the identity
394    /// underneath is a number nobody recognises. Empty when there is nothing to
395    /// add, which is the common case.
396    pub detail: String,
397}
398
399impl Choice {
400    /// One with nothing to disambiguate it.
401    #[must_use]
402    pub fn new(value: impl Into<String>, label: impl Into<String>) -> Self {
403        Self {
404            value: value.into(),
405            label: label.into(),
406            detail: String::new(),
407        }
408    }
409
410    /// One that needs a second line to tell it from another.
411    #[must_use]
412    pub fn detailed(
413        value: impl Into<String>,
414        label: impl Into<String>,
415        detail: impl Into<String>,
416    ) -> Self {
417        Self {
418            value: value.into(),
419            label: label.into(),
420            detail: detail.into(),
421        }
422    }
423}
424
425/// A named list of options an addon offers, which a parameter can draw from.
426///
427/// [ADR-0022]. **Named rather than attached to one parameter**, and the third
428/// reason below is the one that decided it:
429///
430/// - a live fetch is then one addon and one id, where a parameter would need
431///   `(addon, action, param)` — and a *setting*'s parameter has no action;
432/// - two parameters can share one list, so a pin and a priority pointing at the
433///   same people cannot show two different rosters;
434/// - **composition belongs to the addon.** `006-FR-011b` wants the current call,
435///   then people remembered from previous calls, then free text — an ordering
436///   that is Discord's business. With a named source the addon returns one flat
437///   ordered list and the interface renders it, so "remembered people" needs no
438///   interface support at all.
439///
440/// [ADR-0022]: ../../../docs/decisions/0022-addon-supplied-choices.md
441#[derive(Debug, Clone, Copy, PartialEq, Eq)]
442pub struct AddonChoices {
443    /// Stable id, unique within the addon. A parameter names this.
444    pub id: &'static str,
445    /// What the list is, for the editor's heading and its empty state.
446    pub name: &'static str,
447    /// Whether the values must be asked for rather than read from below.
448    ///
449    /// A **declared** source is fixed for the life of the daemon and arrives
450    /// with everything else. A **live** one changes — a voice roster, the
451    /// playlists on an account — and is fetched when somebody opens the picker
452    /// and at no other time. Not polled: a timer would cost idle CPU for a
453    /// picker nobody has open, which `006-FR-027` forbids and `006-SC-008`
454    /// measures.
455    pub live: bool,
456    /// Every value, for a declared source. Empty when [`Self::live`].
457    pub values: &'static [AddonChoice],
458}
459
460/// One thing an action needs to know, declared so an interface can ask for it.
461///
462/// The point of declaring rather than parsing a free-text argument: FR-048
463/// wants addon configuration built from the shared component library, and there
464/// has to be something to build it *from*. A third-party addon gets the same
465/// editor as a first-party one with no code in the interface.
466#[derive(Debug, Clone, Copy, PartialEq, Eq)]
467pub struct AddonParam {
468    /// Stable identifier, and the key a binding stores it under. Renaming one
469    /// silently drops whatever the user had chosen.
470    pub id: &'static str,
471    /// What to call it.
472    pub name: &'static str,
473    /// What it is for, in a sentence.
474    pub description: &'static str,
475    /// What it holds.
476    pub kind: ParamKind,
477    /// Whether the action can run without it.
478    ///
479    /// An optional parameter is a real thing rather than an oversight: the
480    /// media addon's target application is absent for "whatever is playing",
481    /// which is the behaviour most people want most of the time.
482    pub required: bool,
483    /// Whether it holds several values rather than one (`006-FR-024a-i`).
484    ///
485    /// Orthogonal to [`Self::kind`] on purpose. What a value *means* and how
486    /// many of them there are are different questions, and folding one into the
487    /// other is what produces `AppList`, `ParticipantList`, `PlaylistList` —
488    /// one new kind every time somebody wants a list of something.
489    pub multiple: bool,
490    /// The id of an [`AddonChoices`] this draws its options from, if any.
491    ///
492    /// `None` is free text, which is what every parameter was before ADR-0022
493    /// and what a choice-backed one degrades to when its list is unavailable.
494    pub choices: Option<&'static str>,
495}
496
497impl AddonParam {
498    /// A parameter with nothing set, to build from.
499    ///
500    /// Exists so the next field added here does not break every `const` array
501    /// an addon writes: `AddonParam { id: "app", ..AddonParam::BASE }` compiles
502    /// in a `const`, and adding a field to `BASE` costs its authors nothing.
503    /// **It does not save the arrays that already name every field**, which is
504    /// why the ones in this repository were converted when this was added.
505    pub const BASE: Self = Self {
506        id: "",
507        name: "",
508        description: "",
509        kind: ParamKind::Text,
510        required: false,
511        multiple: false,
512        choices: None,
513    };
514}
515
516/// One thing an addon can be asked to do.
517#[derive(Debug, Clone, Copy, PartialEq, Eq)]
518pub struct AddonAction {
519    /// Stable identifier. This is what a binding stores, so it must not change
520    /// once anyone could have saved it — a rename silently breaks their key.
521    pub id: &'static str,
522    /// What to call it in a menu.
523    pub name: &'static str,
524    /// What it does, in a sentence.
525    pub description: &'static str,
526    /// Whether it wants a press or a position.
527    pub trigger: Trigger,
528    /// What it needs to know, if anything.
529    pub params: &'static [AddonParam],
530    /// The step outside Nobble that has to happen for this to work, if there
531    /// is one.
532    ///
533    /// The same field [`DeviceAction`] carries, and for the same reason: a
534    /// dependency Nobble cannot verify has to be *stated*, and an interface
535    /// must not claim the key works until the user confirms it. `None` where
536    /// there is nothing to arrange elsewhere, which is almost every action.
537    ///
538    /// **It belongs to the action, so an action that only sometimes needs one
539    /// is two actions.** A prerequisite shown on a binding that does not need
540    /// it teaches people to dismiss the ones that do.
541    pub prerequisite: Option<&'static str>,
542}
543
544impl AddonAction {
545    /// An action with nothing set, to build from.
546    ///
547    /// Exists for the reason [`AddonParam::BASE`] does, learned the same way:
548    /// this struct gained a field, and every `const` array declaring one had to
549    /// be edited to say nothing had changed. With `..AddonAction::BASE` the
550    /// next field costs nobody anything.
551    ///
552    /// The trigger defaults to [`Trigger::Momentary`] because most actions are
553    /// presses — but it is the one field worth writing out anyway, since a
554    /// continuous action silently declared as a press is bindable to a key that
555    /// can never supply it a position.
556    pub const BASE: Self = Self {
557        id: "",
558        name: "",
559        description: "",
560        trigger: Trigger::Momentary,
561        params: &[],
562        prerequisite: None,
563    };
564
565    /// One parameter's declaration, by id.
566    #[must_use]
567    pub fn param(&self, id: &str) -> Option<&'static AddonParam> {
568        self.params.iter().find(|p| p.id == id)
569    }
570}
571
572/// One keystroke, exactly as the device will send it.
573///
574/// # Why this crate spells the HID vocabulary again
575///
576/// It has no choice. Nothing here may depend on another Nobble crate — the rule
577/// at the top of this module, and FR-044 behind it — so `nobble_core::HidAction`
578/// is unreachable from an addon author's build, and Constitution VI's preferred
579/// answer, generated bindings, does not cross a repository boundary that exists
580/// on purpose (FR-043, FR-049). The alternative to restating the shape is not
581/// restating it somewhere better; it is being unable to declare a keystroke at
582/// all, and then FR-024 has no answer.
583///
584/// What *is* a choice is how much gets restated and how the copy is held in
585/// step. These variants, their field names and their serde spelling in
586/// [`KeystrokeDecl`](crate::protocol::KeystrokeDecl) match
587/// `nobble_rpc::ActionDto::HidTap` and `HidConsumer` one for one, so the object
588/// crossing this pipe is the same JSON text a binding is saved as, and the
589/// daemon's conversion is a rename-free `match` a reader can check by eye. A
590/// test in `nobble-service` — the only crate that can see both — pins that, and
591/// stands in for the generated binding.
592///
593/// **That is weaker than one definition and is recorded as such rather than
594/// argued away.** Nothing makes a new `HidAction` variant a compile error here,
595/// because Cargo cannot see across the boundary. Two things bound the damage:
596/// the drift is asymmetric — a variant added there narrows what this can say, a
597/// variant added here breaks the daemon's exhaustive bridge — and what is copied
598/// is USB HID's vocabulary rather than Nobble's, so it is not a format anyone
599/// here is free to change.
600///
601/// # Why exactly these two, and why not MIDI
602///
603/// The cut is made by an existing function rather than by judgement.
604/// `ActionDto::from_binding` has arms for `HidTap` and `HidConsumer`, and its
605/// `_ => return None` covers key sequences and mouse movement, which have no
606/// on-disk form — so an addon declaring one would declare a binding
607/// `check_supported` refuses to save, and it fails the **whole file** rather
608/// than the one key.
609///
610/// The narrower reading matters as much. One chord cannot type a string, and a
611/// sequence is precisely the mechanism the contract rules out when it says an
612/// addon able to ask the device to send anything at any time *"would be a
613/// keylogger with extra steps"*. Excluding it in the type leaves no check for
614/// anyone to forget.
615///
616/// MIDI is left out on different grounds, and the omission is **not** a claim
617/// that device-resolved means keyboard. `Binding::Midi` is device-resolved too
618/// and a control change is inherently continuous, so resolution and trigger are
619/// genuinely independent axes. But `midi_note` and `midi_cc` are already
620/// first-class binding kinds with their own editors, so there is no
621/// discoverability gap for an addon to close. Adding a variant later is
622/// additive; adding one now is speculative.
623///
624/// Deliberately **not** `#[non_exhaustive]` — the opposite choice from
625/// [`ParamKind`], for the reason that separates them: an unfamiliar parameter
626/// kind has a useful fallback, a text box, and an unfamiliar keystroke has none.
627/// A wildcard arm here is how a device silently sends nothing. Adding a variant
628/// *should* fail to compile in the daemon that has to translate it, which is
629/// what [`Request`](crate::protocol::Request) says for itself.
630#[derive(Debug, Clone, Copy, PartialEq, Eq)]
631pub enum DeviceKeystroke {
632    /// Press and release one key, with modifiers held.
633    ///
634    /// **What is declared here is a suggestion the user is expected to change**,
635    /// and an addon that treats it as a contract has misunderstood the field.
636    /// Discord ships no keybind for Toggle Mute — the user invents one — so
637    /// whatever is declared has to be transcribed into Discord by hand anyway.
638    /// [`Self::Consumer`] is the opposite case: there the addon knows the answer.
639    Tap {
640        /// A HID usage code, **not** a character — the same distinction
641        /// `HidKey` makes on the daemon side, for the same reason: a keystroke
642        /// authored on a QWERTZ layout and pressed on a QWERTY host produces a
643        /// different character, and hiding that makes it invisible until
644        /// somebody complains. Usage `0x10` is the key labelled **M** on ANSI
645        /// and the key labelled **,** on AZERTY, so an addon's choice of chord
646        /// can collide with something on a layout its author never saw. That is
647        /// what the editor is for.
648        key: u8,
649        /// Held with it.
650        ctrl: bool,
651        /// Held with it.
652        shift: bool,
653        /// Held with it.
654        alt: bool,
655        /// Held with it — Windows, Command, Super.
656        gui: bool,
657    },
658    /// A Consumer Control usage — the media keys.
659    ///
660    /// Here because [ADR-0021] intends `media` to gain a device-resolved
661    /// play/pause as a *new* action, and calls its case stronger than Discord's:
662    /// a consumer usage is layout-free and needs nothing arranged elsewhere, so
663    /// the addon genuinely knows the number and the declaration is a fact rather
664    /// than a suggestion. A type that could not say so would have made the
665    /// adoption impossible.
666    ///
667    /// [ADR-0021]: ../../../docs/decisions/0021-addon-device-resolved-actions.md
668    Consumer {
669        /// The usage.
670        usage: u16,
671    },
672}
673
674/// One thing an addon *names* and the **device** does, with no addon running.
675///
676/// [ADR-0021]. Discord's Toggle Mute is the case this exists for: it is a
677/// *global* keybind, so a device that sends it controls Discord from the
678/// background — at the login screen, inside a full-screen game, and with Nobble
679/// closed. That is Principle V and [ADR-0007], and a stronger promise than any
680/// addon can otherwise make.
681///
682/// Binding one writes a HID binding. [`Addon::perform`] is never called, the
683/// addon need not be running, and it need not ever have been *allowed* to run —
684/// ADR-0021 exempts these from the permission grant, because the grant's only
685/// lever is *do not start the process* and a keystroke in flash starts none.
686/// That is defensible only because what the device will send is disclosed when
687/// the key is bound and frozen there.
688///
689/// # Why this is not [`AddonAction`] with more fields
690///
691/// Because the two are not the same shape, and one struct would carry fields
692/// that are load-bearing in one regime and meaningless in the other, with
693/// nothing but a doc comment saying which.
694///
695/// A device-resolved action has no [`Trigger`]: a keystroke has no position to
696/// send, so continuous is incoherent *by construction* rather than merely
697/// disallowed. It has no [`AddonParam`]s: a resolved binding carries no
698/// parameters at all, so a runtime-varying one is impossible here rather than
699/// late. ADR-0021 calls both impossible, and this is the shape in which they
700/// cannot be *written down* — which is the discipline `Binding` already states
701/// for itself, that the kind and the payload cannot be constructed disagreeing.
702///
703/// The honest limit of that: an addon written in something other than Rust can
704/// still put `"trigger"` and `"params"` in the JSON, because serde ignores
705/// fields it was not asked about. What it cannot do is make them *mean*
706/// anything — there is nowhere for the daemon to read them from, so there is no
707/// check to forget. That is a weaker claim than "unrepresentable" and it is the
708/// true one.
709///
710/// The cost of a second list is real and is paid in one place: **ids share one
711/// namespace with [`Addon::actions`]**, because a binding stores one string and
712/// cannot say which list it came from. A collision has no type to prevent it,
713/// and the daemon must refuse it by name rather than guess — otherwise binding
714/// the device-resolved twin resolves to the host-resolved one, which is the
715/// *"silently fall back to a host-resolved call when the daemon happens to be
716/// running"* the contract forbids outright. Worth knowing that ids within
717/// [`Addon::actions`] are not checked for uniqueness today either, and
718/// `Registry::perform` takes the first match; this makes an existing unenforced
719/// invariant load-bearing rather than inventing a new hazard.
720///
721/// # A separate list here is not a separate group in the interface
722///
723/// ADR-0021 requires these to appear in the addon's action list *alongside* the
724/// host-resolved ones, reported per row rather than segregated, because grouping
725/// by mechanism is what FR-024b forbids in the very addon that will hold both
726/// families. Nothing here decides that: the daemon assembles one list for the
727/// interface out of both, the way it already computes `applies` per row rather
728/// than reading it from a declaration. What an author writes and what a user
729/// reads have never been the same shape.
730///
731/// [ADR-0021]: ../../../docs/decisions/0021-addon-device-resolved-actions.md
732/// [ADR-0007]: ../../../docs/decisions/0007-input-delivery.md
733#[derive(Debug, Clone, Copy, PartialEq, Eq)]
734pub struct DeviceAction {
735    /// Stable identifier, and what the binding records as provenance so that
736    /// removing the addon reports the binding broken rather than leaving a bare
737    /// keystroke nobody can explain (`003-FR-066` with `003-FR-047`). It must
738    /// not change once anyone could have bound it, and it must not collide with
739    /// an [`AddonAction::id`] on the same addon.
740    pub id: &'static str,
741    /// What to call it in a menu. The whole reason not to leave this as a
742    /// hand-configured HID binding: the daemon's own summary of a raw tap is
743    /// `Ctrl+Shift+usage 0x10`, which is accurate and unexplainable.
744    pub name: &'static str,
745    /// What it does, in a sentence.
746    pub description: &'static str,
747    /// What the device sends **by default**. The user can change it, and must be
748    /// able to, because it has to match whatever they set in the other
749    /// application and only they know what that is.
750    ///
751    /// Read once, when the key is bound, and never re-resolved on load
752    /// (ADR-0021). So a later version of the addon cannot change what an
753    /// already-bound key types — and a wrong default is wrong for everyone who
754    /// already bound it, with no upgrade path short of rebinding. That is the
755    /// right way round: the alternative is an addon update silently altering a
756    /// key the user authored.
757    pub keystroke: DeviceKeystroke,
758    /// The step outside Nobble that has to happen for this to work, if there is
759    /// one: *"Set this keybind in Discord: User Settings > Keybinds."*
760    ///
761    /// Free text, shown at the moment of binding, and **never parsed** — which
762    /// is what lets it also carry a suggested chord in prose without that
763    /// suggestion becoming a grammar with a compatibility surface of its own.
764    /// The day something reads a keystroke out of this string, this field has
765    /// quietly become the interface [`DeviceKeystroke`] exists to be instead.
766    ///
767    /// `None` where there is nothing to arrange elsewhere, which is the case a
768    /// future device-resolved play/pause is in. An [`Option`] rather than an
769    /// empty string, because the two states oblige the interface differently:
770    /// `Some` means it must not claim the key works until the user confirms the
771    /// step (FR-024), since Nobble cannot read another application's settings
772    /// and must say so rather than pretend. A sentinel would make *the author
773    /// left it blank* and *there is nothing to do* the same value.
774    pub prerequisite: Option<&'static str>,
775}
776
777/// What an action was given when it ran.
778///
779/// Two sources, and keeping them apart is the whole design. **Parameters** were
780/// chosen when the binding was authored and are saved with it; the **value**
781/// was produced by the input a moment ago and is saved nowhere. Merging them
782/// into one map would let a fader position be persisted, which is the sort of
783/// thing that works until someone restarts the daemon and their volume jumps.
784#[derive(Debug, Clone, Copy)]
785pub struct Invocation<'a> {
786    params: &'a BTreeMap<String, ParamValue>,
787    value: Option<u16>,
788    input: Option<&'a str>,
789}
790
791/// Nothing configured, for an action that takes no parameters.
792static NO_PARAMS: std::sync::LazyLock<BTreeMap<String, ParamValue>> =
793    std::sync::LazyLock::new(BTreeMap::new);
794
795impl<'a> Invocation<'a> {
796    /// A press, with the binding's parameters.
797    #[must_use]
798    pub fn press(params: &'a BTreeMap<String, ParamValue>) -> Self {
799        Self {
800            params,
801            value: None,
802            input: None,
803        }
804    }
805
806    /// A position, with the binding's parameters.
807    #[must_use]
808    pub fn moved(params: &'a BTreeMap<String, ParamValue>, value: u16) -> Self {
809        Self {
810            params,
811            value: Some(value),
812            input: None,
813        }
814    }
815
816    /// A press with nothing configured. For tests, and for actions that take
817    /// no parameters.
818    #[must_use]
819    pub fn bare() -> Self {
820        Self {
821            params: &NO_PARAMS,
822            value: None,
823            input: None,
824        }
825    }
826
827    /// Say which input this came from.
828    ///
829    /// A builder rather than a fourth argument, so every existing call still
830    /// compiles and reads the same. The host adds it; an addon never does.
831    #[must_use]
832    pub fn from(mut self, input: &'a str) -> Self {
833        self.input = Some(input);
834        self
835    }
836
837    /// Which input fired, as the same opaque string
838    /// [`Addon::bound_inputs`](crate::Addon::bound_inputs) lists.
839    ///
840    /// `None` when the interface asked rather than a key: there is no input,
841    /// and inventing one would name a key that does not exist.
842    ///
843    /// **Opaque, and meant to stay that way.** It is a key to match against the
844    /// ordered list, not something to parse — the *order* is the daemon's
845    /// answer, because only the daemon knows where the modules physically are.
846    #[must_use]
847    pub fn input(&self) -> Option<&str> {
848        self.input
849    }
850
851    /// One parameter, if the binding set it.
852    ///
853    /// Absent and empty are the same answer here. A text box someone cleared
854    /// stores `""`, and an addon checking only for absence would then treat a
855    /// deliberately blank field as a configured one.
856    /// **A parameter holding a list reads as absent here**, rather than as its
857    /// first entry. See [`ParamValue::one`]: an addon that declared one value
858    /// and silently used the first of several would mute one of the three
859    /// applications somebody named and report nothing.
860    #[must_use]
861    pub fn param(&self, id: &str) -> Option<&str> {
862        self.params.get(id).and_then(ParamValue::one)
863    }
864
865    /// Every value of one parameter, whether it holds one or several.
866    ///
867    /// The accessor a `multiple` parameter reads. A single value reads as a
868    /// list of one, so an addon does not have to care how the user happened to
869    /// fill it in — and a configuration written before lists existed contains
870    /// exactly that case.
871    #[must_use]
872    pub fn param_all(&self, id: &str) -> Vec<&str> {
873        self.params.get(id).map(ParamValue::all).unwrap_or_default()
874    }
875
876    /// The map as it was stored, for a host that has to forward it verbatim.
877    ///
878    /// Not for addons: an addon wants [`Self::param`] or
879    /// [`Self::param_all`]. This exists because the daemon builds an
880    /// invocation on one side of a pipe and has to put the same thing back on
881    /// the wire on the other, and re-deriving it from the accessors would turn
882    /// a list into whatever the accessors happened to flatten it to.
883    #[must_use]
884    pub fn raw_params(&self) -> &BTreeMap<String, ParamValue> {
885        self.params
886    }
887
888    /// Every parameter, for reporting.
889    ///
890    /// A list renders comma-separated, because this feeds a log line rather
891    /// than anything that parses it back.
892    pub fn params(&self) -> impl Iterator<Item = (&str, String)> {
893        self.params
894            .iter()
895            .map(|(k, v)| (k.as_str(), v.all().join(", ")))
896    }
897
898    /// The raw position, `0..=16383`, for a continuous action.
899    #[must_use]
900    pub fn value(&self) -> Option<u16> {
901        self.value
902    }
903
904    /// The position as a fraction of full travel, `0.0..=1.0`.
905    ///
906    /// Provided so the common case is one call rather than a division everyone
907    /// writes slightly differently. **Not** a taper: audio wants a curve, and
908    /// only the addon knows whether its target is already logarithmic.
909    #[must_use]
910    pub fn fraction(&self) -> Option<f32> {
911        self.value.map(|v| f32::from(v) / f32::from(FADER_MAX))
912    }
913}
914
915/// One thing an addon needs configuring once, rather than per key.
916///
917/// [ADR-0013]. A client id, an endpoint, a token. Declared with the same
918/// vocabulary as an action's parameters so the interface renders a settings
919/// page with no code per addon, which is the same FR-048 reasoning.
920///
921/// [ADR-0013]: ../../../docs/decisions/0013-addon-settings-and-secrets.md
922#[derive(Debug, Clone, Copy, PartialEq, Eq)]
923pub struct AddonSetting {
924    /// What to ask for.
925    pub param: AddonParam,
926    /// Whether this is a credential.
927    ///
928    /// A secret is kept in the OS credential store rather than in a file, and
929    /// **is never sent back** — the interface can set one and can ask whether
930    /// one exists, and cannot read it. FR-026 makes profiles shareable, and a
931    /// refresh token that could be read back is one that ends up in a paste.
932    pub secret: bool,
933}
934
935/// Something an addon needs to be allowed to do (FR-046).
936///
937/// Declared as a set the addon needs *in order to work*, not as a wish list:
938/// the user grants or refuses the whole declaration, because a half-granted
939/// addon is a matrix of broken states nobody asked for and every one of them
940/// would have to be designed.
941///
942/// # Only one of these is enforced, and this type does not pretend otherwise
943///
944/// [`Self::Credentials`] is real: the daemon holds the credential store and an
945/// addon can only ask, so a refusal is a refusal. The other three are
946/// **declarations shown to the user**, and an ungranted addon is simply not
947/// started — which is a genuine control, because a process that is not running
948/// opens no sockets. What is *not* true is that a *running* addon is confined
949/// to what it declared. Nothing stops a granted addon reaching a host it never
950/// mentioned.
951///
952/// That gap is [ADR-0016]'s, deliberately, and it is named in the interface
953/// rather than papered over: "may reach api.spotify.com" must not be read as
954/// "and nothing else" until platform confinement makes it true.
955///
956/// [ADR-0016]: ../../../docs/decisions/0016-addon-process-boundary.md
957#[derive(Debug, Clone, Copy, PartialEq, Eq)]
958#[non_exhaustive]
959pub enum Permission {
960    /// Reach a host over the network.
961    ///
962    /// One host per declaration rather than a list, so the interface can show
963    /// them as separate lines and so a diff between two versions of an addon
964    /// reads as "it now also wants X".
965    Network {
966        /// The host, as it appears in a URL: `api.spotify.com`.
967        host: &'static str,
968        /// Why, in a few words, shown next to it. An addon that cannot explain
969        /// what it wants a host for is asking the user to guess.
970        reason: &'static str,
971    },
972    /// Read or write files under a path.
973    Files {
974        /// The directory or file, as a path the user would recognise.
975        path: &'static str,
976        /// Whether it writes, or only reads. The distinction is the whole
977        /// difference between "reads your project list" and "can delete it".
978        write: bool,
979        /// Why.
980        reason: &'static str,
981    },
982    /// Start another program.
983    Launch {
984        /// What it starts. `the default browser` is a legitimate answer here —
985        /// this is shown to a person, not matched against anything.
986        program: &'static str,
987        /// Why.
988        reason: &'static str,
989    },
990    /// Keep credentials of its own, in the OS credential store.
991    ///
992    /// The enforced one. Ungranted, the daemon answers every credential ask
993    /// with a refusal, which an addon must survive: it is the same answer as
994    /// "nothing stored yet", and an addon that handles being signed out
995    /// already handles this.
996    Credentials {
997        /// Why. "To stay signed in to your Spotify account" — the account is
998        /// the thing the user is actually deciding about.
999        reason: &'static str,
1000    },
1001}
1002
1003impl Permission {
1004    /// Why the addon says it needs this.
1005    #[must_use]
1006    pub const fn reason(&self) -> &'static str {
1007        match self {
1008            Self::Network { reason, .. }
1009            | Self::Files { reason, .. }
1010            | Self::Launch { reason, .. }
1011            | Self::Credentials { reason } => reason,
1012        }
1013    }
1014}
1015
1016/// A named boolean fact an addon publishes about its target.
1017///
1018/// FR-062. Recording, streaming, in a call, muted, playing. An overlay can be
1019/// conditioned on one (ADR-0011), which makes a signal a **resolution input**
1020/// and therefore a stronger obligation than FR-047's display state: a stale
1021/// label is a cosmetic problem, a stale signal is a device doing the wrong
1022/// thing.
1023///
1024/// Boolean, and FR-063 keeps it that way until a specified need exists for
1025/// more. The same reasoning as the RPC being deliberately small: a value set
1026/// that grows on speculation grows a conversion for every consumer, and the
1027/// consumers here include third-party addons.
1028#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1029pub struct AddonSignal {
1030    /// Stable identifier, **unqualified** — `recording`, not `obs.recording`.
1031    /// The addon's own id qualifies it, so an addon cannot claim a name in
1032    /// another's space by choosing a clever string.
1033    ///
1034    /// Stored in configurations, so a rename silently breaks someone's
1035    /// overlay, exactly as [`AddonAction::id`] does.
1036    pub id: &'static str,
1037    /// What to call it.
1038    pub name: &'static str,
1039    /// What it means, in a sentence. Shown next to a live value, so it has to
1040    /// say what *true* means rather than what the signal is about: "Spotify is
1041    /// playing" beats "playback state".
1042    pub description: &'static str,
1043}
1044
1045/// What an addon's signals read at one moment.
1046///
1047/// # Why the daemon asks rather than the addon telling
1048///
1049/// **Polled, not pushed**, and the reasons are all about what happens when
1050/// this interface crosses a process boundary — which FR-045 says it will.
1051///
1052/// A request/response call survives that move unchanged. A callback does not:
1053/// it needs a reverse channel, and a reverse channel is precisely where a
1054/// wedged addon becomes a wedged daemon, because something has to be waiting
1055/// on it.
1056///
1057/// Polling also puts the rate limit on the side that suffers from getting it
1058/// wrong. A pushing addon that reports a flapping signal thousands of times a
1059/// second is a churn source the daemon can only damp *after* paying for it,
1060/// and ADR-0011 is explicit that signal churn is flash wear rather than a slow
1061/// link. A polled addon cannot produce that by construction.
1062///
1063/// What polling costs is answered by the addon, not by the poll rate: an
1064/// implementation is free to keep a cache fed by platform events and answer
1065/// from it, which is what FR-065's "no measurable idle CPU" actually turns on.
1066#[derive(Debug, Clone, PartialEq, Eq)]
1067pub enum Reading {
1068    /// It looked, and these are the values.
1069    ///
1070    /// Ids must be ones [`Addon::signals`] declares. Anything else is dropped
1071    /// by the daemon rather than becoming a signal nobody can find the
1072    /// definition of.
1073    Values(Vec<(&'static str, bool)>),
1074    /// It could not look, and this is why.
1075    ///
1076    /// FR-064: every signal it publishes then reads false, **and the reason is
1077    /// this sentence** rather than "the condition was false". The two states
1078    /// are indistinguishable to a user and have different fixes — one is a
1079    /// configuration mistake, the other is a closed application.
1080    Unavailable(String),
1081}
1082
1083impl Reading {
1084    /// A reading of nothing, from an addon that publishes no signals.
1085    #[must_use]
1086    pub fn none() -> Self {
1087        Self::Values(Vec::new())
1088    }
1089
1090    /// The value of one signal, if this reading carries it.
1091    #[must_use]
1092    pub fn get(&self, signal: &str) -> Option<bool> {
1093        match self {
1094            Self::Values(values) => values.iter().find(|(id, _)| *id == signal).map(|(_, v)| *v),
1095            Self::Unavailable(_) => None,
1096        }
1097    }
1098}
1099
1100/// Whether an addon can act at the moment.
1101#[derive(Debug, Clone, PartialEq, Eq)]
1102pub enum Availability {
1103    /// It can.
1104    Ready,
1105    /// It cannot, and this is why, in words a user can act on.
1106    ///
1107    /// "Spotify is not running" is useful. "Error 0x80070002" is not: a person
1108    /// reading it on a settings page cannot do anything with it.
1109    Unavailable(String),
1110}
1111
1112/// Why performing an action failed.
1113#[derive(Debug, Clone, PartialEq, Eq)]
1114pub enum AddonError {
1115    /// No action by that name. Usually a binding saved against an older
1116    /// version of the addon.
1117    NoSuchAction(String),
1118    /// The addon could not act right now.
1119    Unavailable(String),
1120    /// It tried and something went wrong.
1121    Failed(String),
1122}
1123
1124impl fmt::Display for AddonError {
1125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1126        match self {
1127            Self::NoSuchAction(a) => write!(f, "no action called {a:?}"),
1128            // Both render as their reason. The variants stay separate because
1129            // callers distinguish them -- unavailable is worth retrying, failed
1130            // is not -- but a reader wants the sentence, not the category.
1131            Self::Unavailable(why) | Self::Failed(why) => write!(f, "{why}"),
1132        }
1133    }
1134}
1135
1136impl std::error::Error for AddonError {}
1137
1138/// Something that can act on another application's behalf.
1139///
1140/// Implementations are expected to be cheap to construct and to discover their
1141/// target lazily: the daemon builds every addon at startup whether or not the
1142/// application it integrates is installed, and a constructor that blocked on a
1143/// network call would delay login for all of them.
1144pub trait Addon: Send {
1145    /// Stable identifier, stored in bindings. Never rename one.
1146    fn id(&self) -> &'static str;
1147
1148    /// What to call it.
1149    fn name(&self) -> &'static str;
1150
1151    /// What it integrates, in a sentence.
1152    fn description(&self) -> &'static str;
1153
1154    /// Everything it can be asked to do.
1155    fn actions(&self) -> &'static [AddonAction];
1156
1157    /// Everything it *names* that the device does on its own (ADR-0021).
1158    ///
1159    /// Defaulted to nothing, like [`Self::signals`] and [`Self::settings`], and
1160    /// for a stronger reason than either: almost every addon has none, and the
1161    /// whole point of the declaration is that it is exceptional. An addon
1162    /// listing one is saying *this is better as a keystroke than as a call to
1163    /// me, and here is what to call it*.
1164    ///
1165    /// **[`Self::perform`] is never called for one of these** by a daemon that
1166    /// understands the declaration — it resolved the action to a keystroke when
1167    /// the key was bound, and the press never reaches the host at all. An author
1168    /// implements nothing for them and must not try. A daemon built against an
1169    /// older SDK dropped this field on the way in and will call `perform`
1170    /// anyway; [`run`](crate::run) answers that itself with a failure naming the
1171    /// cause, so an author still implements nothing.
1172    ///
1173    /// Ids share one namespace with [`Self::actions`], because a binding stores
1174    /// one string and cannot say which list it came from.
1175    fn device_actions(&self) -> &'static [DeviceAction] {
1176        &[]
1177    }
1178
1179    /// Whether it could act right now.
1180    ///
1181    /// Asked whenever the interface draws the addon, so it must be quick and
1182    /// must not block. It is allowed to be wrong a moment later — the target
1183    /// application can close between this answer and the next keypress, which
1184    /// is why [`Self::perform`] returns a result of its own rather than
1185    /// trusting this.
1186    fn availability(&self) -> Availability;
1187
1188    /// Do it.
1189    ///
1190    /// `invocation` carries the binding's parameters and, for a
1191    /// [`Trigger::Continuous`] action, the position the input reported.
1192    ///
1193    /// # Errors
1194    /// If the action is unknown, a required parameter is missing, the target is
1195    /// unavailable, or it failed.
1196    fn perform(&mut self, action: &str, invocation: &Invocation<'_>) -> Result<(), AddonError>;
1197
1198    /// Whether an action is worth offering at the moment.
1199    ///
1200    /// For the pair that undo each other — sign in and sign out — where
1201    /// offering both at once means one of them is always the wrong half of a
1202    /// question nobody asked.
1203    ///
1204    /// # This is relevance, not permission
1205    ///
1206    /// [`Self::perform`] deliberately does **not** consult it, and neither
1207    /// does [`Self::actions`], which stays complete. Three consequences, all
1208    /// intended:
1209    ///
1210    /// - A key bound to a hidden action still works. Bindings are authored
1211    ///   once and pressed later, and a key that stopped existing because of
1212    ///   something that happened after it was bound is the failure US9 calls
1213    ///   out.
1214    /// - The action can still be *bound*, because the editor is about what a
1215    ///   key could ever do rather than what is useful this second.
1216    /// - An addon must still handle being asked. Signing out twice succeeds;
1217    ///   this only stops the interface suggesting it.
1218    ///
1219    /// Defaulted to `true`, which is the ordinary case: an action that is
1220    /// worth having is worth having now.
1221    fn applies(&self, _action: &str) -> bool {
1222        true
1223    }
1224
1225    /// One line about what it is currently connected to, when that is a
1226    /// question a user can have.
1227    ///
1228    /// Shown wherever the addon is drawn, next to its name.
1229    ///
1230    /// # Why this is not part of [`Availability`]
1231    ///
1232    /// They answer different questions, and only one of them has an answer
1233    /// when things are fine. `Availability` is *can it act*, and the useful
1234    /// case is the negative one — an addon that cannot act owes the user a
1235    /// reason. `Ready` carries no words because "it works" needs none.
1236    ///
1237    /// This is *what is it working as*, and it is only interesting when the
1238    /// answer is **ready**. "Signed in as Philipp" tells someone which of two
1239    /// Spotify accounts their Save key is filling up, which is invisible from
1240    /// anywhere else in the interface and is exactly the thing they need before
1241    /// pressing Sign out. Folding it into `Ready(String)` would have made every
1242    /// addon that has nothing to say construct an empty one.
1243    ///
1244    /// Defaulted to `None`, which is the ordinary case: an addon that talks to
1245    /// a local application is connected to the only thing it could be.
1246    ///
1247    /// Called whenever the interface draws the addon, so it must be quick and
1248    /// must not block — the same contract as [`Self::availability`], and the
1249    /// same reason. Read it from what the addon already holds; do not go and
1250    /// ask the network.
1251    fn status(&self) -> Option<String> {
1252        None
1253    }
1254
1255    /// Every signal it publishes. FR-062.
1256    ///
1257    /// Defaulted, because most addons only act. An addon that publishes none
1258    /// is not a lesser addon — it is the ordinary case, and the interface
1259    /// should not make it write an empty slice to say so.
1260    fn signals(&self) -> &'static [AddonSignal] {
1261        &[]
1262    }
1263
1264    /// Everything it needs configuring once (ADR-0013).
1265    ///
1266    /// Defaulted for the same reason as signals: most addons need nothing, and
1267    /// the ones that do are the exception.
1268    fn settings(&self) -> &'static [AddonSetting] {
1269        &[]
1270    }
1271
1272    /// The inputs bound to one of this addon's actions, **in device order**.
1273    ///
1274    /// Sent when it changes: a binding edited, a profile switched, a module
1275    /// attached or unplugged. Defaulted to ignoring it, because almost no addon
1276    /// cares which key called it.
1277    ///
1278    /// # Why the daemon sends an order rather than positions
1279    ///
1280    /// `006-FR-014a` defines fader order as *"ascending slot position, then
1281    /// input index within the module"* — and the only side that knows where a
1282    /// module physically sits is the daemon, which owns the inventory. Sending
1283    /// coordinates would make every addon that cares reimplement the sort, and
1284    /// the second implementation would be the one that was wrong about a
1285    /// module attached at a negative offset.
1286    ///
1287    /// So this is already sorted. Match [`Invocation::input`] against it to
1288    /// learn which fader moved and where it sits among the others.
1289    fn bound_inputs(&mut self, action: &str, inputs: &[String]) {
1290        let _ = (action, inputs);
1291    }
1292
1293    /// Which of this action's inputs the user currently has hold of.
1294    ///
1295    /// Sent when the set changes, not on a timer. Defaulted to ignoring it,
1296    /// because almost no addon cares — it matters only where the host *moves*
1297    /// an input on its own, which today means a motorised fader.
1298    ///
1299    /// # Why an addon is told rather than asked
1300    ///
1301    /// Held is defined against something the **device** reports, and only the
1302    /// host sees the device. An addon deriving it from the values it receives
1303    /// would be doing it from the wrong side of a pipe and against a different
1304    /// set of reports, and two definitions of held would disagree exactly when
1305    /// it mattered.
1306    ///
1307    /// # What it is for
1308    ///
1309    /// A host-driven control moving under somebody's hand is the case where
1310    /// automation stops feeling like automation. An addon that reassigns
1311    /// controls should leave a held one alone and apply the change when it is
1312    /// released — **and apply the latest one**, not the one that was pending
1313    /// when it was grabbed. The user let go into the present.
1314    fn held_inputs(&mut self, _action: &str, _held: &[String]) {}
1315
1316    /// Named lists its parameters can draw options from (ADR-0022).
1317    ///
1318    /// Defaulted to nothing, because most parameters are free text and always
1319    /// were. Declared here rather than on the parameter so that two parameters
1320    /// can share one list — a pin and a priority pointing at the same people
1321    /// must not be able to show two different rosters.
1322    ///
1323    /// A source marked [`AddonChoices::live`] carries no values here; the
1324    /// daemon asks for them when somebody opens the picker.
1325    fn choices(&self) -> &'static [AddonChoices] {
1326        &[]
1327    }
1328
1329    /// The current options for a [`live`](AddonChoices::live) source.
1330    ///
1331    /// Asked **when somebody opens the picker, and at no other time.** Not
1332    /// polled: a timer would cost idle CPU for a picker nobody has open. So
1333    /// this is allowed to be the slow one — but it still must not block for
1334    /// long, because an interface is waiting on it with nothing to draw.
1335    ///
1336    /// Answer from whatever the addon already knows rather than by asking the
1337    /// far side. An addon that holds a cached picture answers from memory; one
1338    /// that must fetch should keep the request small and give up quickly rather
1339    /// than leave a menu spinning.
1340    ///
1341    /// **The order is the addon's to decide**, and that is the point of a named
1342    /// source rather than a per-parameter one: an addon that wants *who is here
1343    /// now*, then *who I have seen before*, then nothing, returns one flat
1344    /// ordered list and the interface renders it in that order. No interface
1345    /// support is needed for a concept only the addon has.
1346    ///
1347    /// An unknown id answers empty rather than panicking — the daemon and the
1348    /// addon can disagree across a version, and a menu with nothing in it is a
1349    /// better outcome than a dead child process.
1350    fn live_choices(&mut self, _id: &str) -> Vec<Choice> {
1351        Vec::new()
1352    }
1353
1354    /// Everything it needs to be *allowed* to do (FR-046).
1355    ///
1356    /// Defaulted to nothing, and that default is the honest one for more
1357    /// addons than it looks: an addon that drives a local API — the media
1358    /// session, the audio mixer — reaches no host, opens no file and keeps no
1359    /// account, so it has nothing to ask for and the user is never asked.
1360    ///
1361    /// **An addon declaring anything here does not run until the user grants
1362    /// it.** So the list is what the addon needs, not what it might one day
1363    /// like: every entry is a question somebody has to answer before the addon
1364    /// works at all, and an addon that asks for more than it uses is training
1365    /// people to grant without reading.
1366    fn permissions(&self) -> &'static [Permission] {
1367        &[]
1368    }
1369
1370    /// Take its settings, and a place to keep its own credentials.
1371    ///
1372    /// Called at startup and whenever the settings change, so an addon holds
1373    /// what it needs rather than asking. [`Credentials`] arrives as a handle
1374    /// rather than a value because an addon needs it *later* too — a refreshed
1375    /// token has to go back, and that happens mid-action rather than at
1376    /// configuration time.
1377    fn configure(&mut self, _values: &BTreeMap<String, String>, _credentials: CredentialHandle) {}
1378
1379    /// Take somewhere to remember things that are not secrets (ADR-0027).
1380    ///
1381    /// Called **once, before the first [`Self::configure`]**, so an addon that
1382    /// wants to load what it remembered can do so before it is asked anything.
1383    ///
1384    /// A separate method rather than a third argument to `configure`, because
1385    /// this arrived after the trait was published and widening a signature
1386    /// would break every addon written against the older one — including ones
1387    /// whose authors cannot be contacted. Defaulted to ignoring it, which is
1388    /// the right behaviour for the many addons that remember nothing.
1389    fn attach_store(&mut self, _store: StoreHandle) {}
1390
1391    /// Read every signal it publishes, now.
1392    ///
1393    /// Called on the signal poll, **never on the input path** — the same rule
1394    /// as [`Self::perform`], and for a stronger reason: this runs on a timer,
1395    /// so an implementation that blocked for a second would do it forever
1396    /// rather than once per keypress.
1397    ///
1398    /// See [`Reading`] for why this is a question the daemon asks rather than
1399    /// something the addon announces.
1400    fn read_signals(&mut self) -> Reading {
1401        Reading::none()
1402    }
1403}
1404
1405#[cfg(test)]
1406mod tests {
1407    use super::{ParamKind, Trigger, app_matches};
1408
1409    /// Every variant survives the round trip its own encoder produces.
1410    ///
1411    /// The list is written out rather than iterated, because there is no
1412    /// `Trigger::all()` and inventing one to test with would be a second place
1413    /// that has to know every variant — the failure this whole arrangement is
1414    /// about. A new variant makes `as_wire` fail to compile; this is here so
1415    /// that a *renamed* one, which compiles perfectly, fails something.
1416    #[test]
1417    fn a_trigger_survives_its_own_spelling() {
1418        for t in [Trigger::Momentary, Trigger::Continuous] {
1419            assert_eq!(Trigger::from_wire(t.as_wire()), Some(t), "{t:?}");
1420        }
1421        assert_eq!(Trigger::from_wire("Continuous"), None, "case matters");
1422        assert_eq!(Trigger::from_wire(""), None);
1423    }
1424
1425    /// The tokens themselves, pinned. The round trip above agrees with itself
1426    /// whatever both halves are renamed to; these two strings are compared
1427    /// against by a daemon decoding an addon's declaration and by TypeScript in
1428    /// the settings window, neither of which this crate can see.
1429    #[test]
1430    fn the_trigger_tokens_are_these_two() {
1431        assert_eq!(Trigger::Momentary.as_wire(), "momentary");
1432        assert_eq!(Trigger::Continuous.as_wire(), "continuous");
1433    }
1434
1435    #[test]
1436    fn a_param_kind_survives_its_own_spelling() {
1437        for k in [ParamKind::Text, ParamKind::App] {
1438            assert_eq!(ParamKind::from_wire(k.as_wire()), Some(k), "{k:?}");
1439        }
1440        assert_eq!(ParamKind::from_wire("colour"), None);
1441    }
1442
1443    #[test]
1444    fn the_param_kind_tokens_are_these_two() {
1445        assert_eq!(ParamKind::Text.as_wire(), "text");
1446        assert_eq!(ParamKind::App.as_wire(), "app");
1447    }
1448
1449    #[test]
1450    fn an_application_matches_whichever_form_each_side_is_in() {
1451        // The two sides come from different places and neither will change:
1452        // Nobble's foreground watcher says `exe:spotify.exe`, and the API the
1453        // addon is asking says whatever the application registered with it.
1454        assert!(app_matches("Spotify.exe", "exe:spotify.exe"));
1455        assert!(app_matches(
1456            "SpotifyAB.SpotifyMusic_zpdnekdrzrea0!Spotify",
1457            "exe:spotify.exe"
1458        ));
1459        assert!(app_matches("Spotify.exe", "spotify"));
1460        assert!(app_matches("chrome.exe", "exe:Chrome.exe"));
1461    }
1462
1463    #[test]
1464    fn a_different_application_does_not_match() {
1465        assert!(!app_matches("chrome.exe", "exe:spotify.exe"));
1466        assert!(!app_matches("Spotify.exe", "exe:firefox.exe"));
1467    }
1468
1469    #[test]
1470    fn an_empty_target_matches_nothing_rather_than_everything() {
1471        // Naming a target means "this one". A blank one matching everything
1472        // would turn a mistyped field into a key that controls whatever
1473        // happens to be loudest, which is the failure the parameter exists to
1474        // prevent.
1475        assert!(!app_matches("Spotify.exe", ""));
1476        assert!(!app_matches("Spotify.exe", "exe:"));
1477    }
1478}