Skip to main content

openlogi_core/binding/
effect.rs

1//! A platform-neutral synthesis IR.
2//!
3//! [`Action`] has one variant per user-facing behaviour (52 of them), but the
4//! three `openlogi-inject` backends don't care about most of that
5//! granularity — they care about *mechanism*: "press this chord", "click
6//! this mouse button", "fire this media key", "there is no portable way to
7//! do this, use the OS-specific path". [`Action::effect`] is the single
8//! exhaustive match that sorts every variant into one of those buckets, so
9//! each backend matches on ~10-variant [`Effect`] instead of re-deriving the
10//! full `Action` vocabulary three times.
11//!
12//! [`Shortcut`], [`MediaKey`], and [`NativeAction`] are semantic, not
13//! mechanical: the same named shortcut is not the same chord on every OS
14//! (`BrowserBack` is ⌘\[ on macOS, Alt+Left on Linux, and a dedicated
15//! virtual key with no modifier on Windows), so each backend owns its own
16//! lookup table from these enums to its platform's key/API. The
17//! exhaustiveness check on those enums is what keeps all three backends
18//! honest when a variant is added — a backend that forgets a case fails to
19//! compile rather than silently no-op-ing.
20
21use super::action::{Action, WorkflowStep};
22use super::key_combo::KeyCombo;
23
24/// What firing an [`Action`] should do, independent of platform.
25///
26/// `openlogi_inject`'s per-OS backends match on this instead of on
27/// [`Action`] directly.
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum Effect<'a> {
30    /// Suppress the input entirely: no OS event at all.
31    None,
32    /// Synthesise a physical mouse button click.
33    Click(MouseButton),
34    /// Fire a named shortcut. Each backend maps this to its own platform
35    /// chord (or, for the rare shortcut with no chord at all on some OS, a
36    /// dedicated native code path) — see that backend's `combo` table.
37    Shortcut(Shortcut),
38    /// Press an already-resolved keyboard chord: a user-recorded
39    /// [`Action::CustomShortcut`], or a workflow's `PressKey` step.
40    Key(&'a KeyCombo),
41    /// A user-recorded chord whose output is held by a lifecycle-aware
42    /// runtime. A one-shot executor treats this as [`Effect::Key`] so direct
43    /// dispatch remains balanced when no matching release can arrive.
44    ///
45    /// Which keys the runtime actually holds is the action's business, not
46    /// the backend's: [`Action::HoldShortcut`] holds the whole chord while
47    /// [`Action::TapKeyHoldingModifiers`] holds only the modifiers. Both
48    /// degrade to the same balanced chord tap here, so the split lives in
49    /// [`Action::held_chord`] rather than in this IR.
50    HeldKey(&'a KeyCombo),
51    /// Synthesise one scroll tick. `dx`/`dy` are unit direction (-1/0/1);
52    /// each backend applies its own tick magnitude.
53    Scroll {
54        /// Horizontal direction: -1 left, 1 right, 0 none.
55        dx: i8,
56        /// Vertical direction: -1 down, 1 up, 0 none.
57        dy: i8,
58    },
59    /// Fire a media/volume key. Every backend reaches these through a
60    /// dedicated OS mechanism rather than an ordinary keyboard chord.
61    Media(MediaKey),
62    /// A window-manager or power action with no shared cross-platform
63    /// chord — each backend has its own dedicated handling, which may be a
64    /// debug-logged no-op where the OS has no equivalent at all.
65    Native(NativeAction),
66    /// A power-user scripting escape hatch.
67    Script(Script<'a>),
68    /// Type this text via unicode input.
69    Text(&'a str),
70    /// Handled entirely by the agent/hook layer — DPI presets, SmartShift,
71    /// the Actions Ring, and launching an application. The injector logs
72    /// and does nothing.
73    ///
74    /// [`Action::OpenApplication`] is included here even though
75    /// `openlogi_inject::execute` does open it: that happens in the
76    /// platform-independent dispatcher *before* a backend ever sees the
77    /// action (the config target is opened via the `opener` crate, not
78    /// through any per-OS synthesis path), so from a backend's point of
79    /// view it is exactly as much a no-op as the DPI actions.
80    AgentSide,
81}
82
83/// A physical mouse button an [`Effect::Click`] should press.
84#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
85pub enum MouseButton {
86    /// Primary button.
87    Left,
88    /// Secondary button.
89    Right,
90    /// Wheel-click button.
91    Middle,
92    /// Extra "back" side button (button 4).
93    Back,
94    /// Extra "forward" side button (button 5).
95    Forward,
96}
97
98/// A named shortcut whose chord is the same *concept* on every OS but not
99/// the same keys.
100///
101/// Each backend owns a `Shortcut -> KeyCombo` table (plus, for the couple of
102/// shortcuts a given OS has no ordinary chord for, a small override) rather
103/// than sharing one table — see the per-backend `combo`/`press_shortcut`.
104#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, strum::VariantArray)]
105pub enum Shortcut {
106    /// Copy the selection.
107    Copy,
108    /// Paste from the clipboard.
109    Paste,
110    /// Cut the selection.
111    Cut,
112    /// Undo the last action.
113    Undo,
114    /// Redo the last undone action.
115    Redo,
116    /// Select all content.
117    SelectAll,
118    /// Open find/search.
119    Find,
120    /// Save the current document.
121    Save,
122    /// Navigate backward in browser history.
123    BrowserBack,
124    /// Navigate forward in browser history.
125    BrowserForward,
126    /// Open a new tab.
127    NewTab,
128    /// Close the current tab.
129    CloseTab,
130    /// Reopen the last closed tab.
131    ReopenTab,
132    /// Switch to the next tab.
133    NextTab,
134    /// Switch to the previous tab.
135    PrevTab,
136    /// Reload the current page.
137    ReloadPage,
138}
139
140impl Shortcut {
141    /// Every named shortcut, in declaration order.
142    ///
143    /// `#[derive(strum::VariantArray)]` generates this straight from the
144    /// enum's variant list at compile time, so it cannot go stale the way a
145    /// hand-written array literal could: there is no second, independently
146    /// editable list for it to drift from — a variant that's missing here
147    /// would mean the enum itself doesn't have it. The single shared
148    /// iteration source for each backend's `Shortcut -> KeyCombo`
149    /// table-completeness test.
150    pub const ALL: &'static [Shortcut] = <Shortcut as strum::VariantArray>::VARIANTS;
151}
152
153/// A media/volume key.
154///
155/// Every backend reaches these through a dedicated OS mechanism — NX
156/// system-defined keys on macOS, MPRIS/XF86 keys on Linux, dedicated media
157/// virtual keys on Windows — rather than an ordinary keyboard chord.
158#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
159pub enum MediaKey {
160    /// Toggle play/pause.
161    PlayPause,
162    /// Skip to the next track.
163    NextTrack,
164    /// Go back to the previous track.
165    PrevTrack,
166    /// Increase system volume.
167    VolumeUp,
168    /// Decrease system volume.
169    VolumeDown,
170    /// Toggle system mute.
171    Mute,
172}
173
174/// A window-manager or power action with no shared cross-platform chord.
175///
176/// Each backend reaches it through its own dedicated OS API — or, where the
177/// OS has no equivalent at all, a debug-logged no-op.
178#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
179pub enum NativeAction {
180    /// Show all windows across spaces (macOS Mission Control).
181    MissionControl,
182    /// Show all windows of the frontmost app (macOS App Exposé).
183    AppExpose,
184    /// Switch to the previous desktop/space.
185    PreviousDesktop,
186    /// Switch to the next desktop/space.
187    NextDesktop,
188    /// Hide all windows to reveal the desktop.
189    ShowDesktop,
190    /// Open the application launcher.
191    LaunchpadShow,
192    /// Lock the screen.
193    LockScreen,
194    /// Capture a full-screen screenshot.
195    Screenshot,
196    /// Capture a selected screen region.
197    CaptureRegion,
198    /// Put the computer to sleep.
199    Sleep,
200}
201
202/// A power-user scripting escape hatch, borrowed from the originating
203/// [`Action::RunAppleScript`], [`Action::RunShellCommand`], or
204/// [`Action::Workflow`].
205#[derive(Clone, Copy, Debug, PartialEq, Eq)]
206pub enum Script<'a> {
207    /// Run an AppleScript source string. macOS-only; other backends warn.
208    AppleScript(&'a str),
209    /// Run a shell command string.
210    ShellCommand(&'a str),
211    /// Run an ordered sequence of workflow steps.
212    Workflow(&'a [WorkflowStep]),
213}
214
215impl Action {
216    /// Classify this action into the platform-neutral [`Effect`] IR that
217    /// `openlogi-inject`'s backends dispatch on.
218    ///
219    /// This is the single exhaustive match over the full [`Action`]
220    /// vocabulary that a new backend needs — see the module docs.
221    #[must_use]
222    pub fn effect(&self) -> Effect<'_> {
223        match self {
224            Action::None => Effect::None,
225
226            Action::LeftClick => Effect::Click(MouseButton::Left),
227            Action::RightClick => Effect::Click(MouseButton::Right),
228            Action::MiddleClick => Effect::Click(MouseButton::Middle),
229            Action::MouseBack => Effect::Click(MouseButton::Back),
230            Action::MouseForward => Effect::Click(MouseButton::Forward),
231
232            Action::Copy => Effect::Shortcut(Shortcut::Copy),
233            Action::Paste => Effect::Shortcut(Shortcut::Paste),
234            Action::Cut => Effect::Shortcut(Shortcut::Cut),
235            Action::Undo => Effect::Shortcut(Shortcut::Undo),
236            Action::Redo => Effect::Shortcut(Shortcut::Redo),
237            Action::SelectAll => Effect::Shortcut(Shortcut::SelectAll),
238            Action::Find => Effect::Shortcut(Shortcut::Find),
239            Action::Save => Effect::Shortcut(Shortcut::Save),
240
241            Action::BrowserBack => Effect::Shortcut(Shortcut::BrowserBack),
242            Action::BrowserForward => Effect::Shortcut(Shortcut::BrowserForward),
243            Action::NewTab => Effect::Shortcut(Shortcut::NewTab),
244            Action::CloseTab => Effect::Shortcut(Shortcut::CloseTab),
245            Action::ReopenTab => Effect::Shortcut(Shortcut::ReopenTab),
246            Action::NextTab => Effect::Shortcut(Shortcut::NextTab),
247            Action::PrevTab => Effect::Shortcut(Shortcut::PrevTab),
248            Action::ReloadPage => Effect::Shortcut(Shortcut::ReloadPage),
249
250            Action::MissionControl => Effect::Native(NativeAction::MissionControl),
251            Action::AppExpose => Effect::Native(NativeAction::AppExpose),
252            Action::PreviousDesktop => Effect::Native(NativeAction::PreviousDesktop),
253            Action::NextDesktop => Effect::Native(NativeAction::NextDesktop),
254            Action::ShowDesktop => Effect::Native(NativeAction::ShowDesktop),
255            Action::LaunchpadShow => Effect::Native(NativeAction::LaunchpadShow),
256
257            Action::LockScreen => Effect::Native(NativeAction::LockScreen),
258            Action::Screenshot => Effect::Native(NativeAction::Screenshot),
259            Action::CaptureRegion => Effect::Native(NativeAction::CaptureRegion),
260            Action::Sleep => Effect::Native(NativeAction::Sleep),
261
262            Action::PlayPause => Effect::Media(MediaKey::PlayPause),
263            Action::NextTrack => Effect::Media(MediaKey::NextTrack),
264            Action::PrevTrack => Effect::Media(MediaKey::PrevTrack),
265            Action::VolumeUp => Effect::Media(MediaKey::VolumeUp),
266            Action::VolumeDown => Effect::Media(MediaKey::VolumeDown),
267            Action::MuteVolume => Effect::Media(MediaKey::Mute),
268
269            // DPI/SmartShift/the Actions Ring/OpenApplication are all handled
270            // above (or beside) the injector — see `Effect::AgentSide`.
271            Action::CycleDpiPresets
272            | Action::SetDpiPreset(_)
273            | Action::ToggleSmartShift
274            | Action::ShowActionsRing
275            | Action::OpenApplication(_) => Effect::AgentSide,
276
277            Action::ScrollUp => Effect::Scroll { dx: 0, dy: 1 },
278            Action::ScrollDown => Effect::Scroll { dx: 0, dy: -1 },
279            Action::HorizontalScrollLeft => Effect::Scroll { dx: -1, dy: 0 },
280            Action::HorizontalScrollRight => Effect::Scroll { dx: 1, dy: 0 },
281
282            Action::CustomShortcut(combo) => Effect::Key(combo),
283            Action::HoldShortcut(combo) | Action::TapKeyHoldingModifiers(combo) => {
284                Effect::HeldKey(combo)
285            }
286
287            Action::TypeText(text) => Effect::Text(text),
288            Action::RunAppleScript(src) => Effect::Script(Script::AppleScript(src)),
289            Action::RunShellCommand(cmd) => Effect::Script(Script::ShellCommand(cmd)),
290            Action::Workflow(steps) => Effect::Script(Script::Workflow(steps)),
291        }
292    }
293}