Skip to main content

openlogi_core/binding/
action.rs

1//! The action vocabulary a button can bind to, plus workflow steps.
2
3use serde::{Deserialize, Serialize};
4
5use super::application_target::ApplicationTarget;
6use super::category::Category;
7use super::key_combo::KeyCombo;
8
9/// What pressing a [`ButtonId`] should do.
10///
11/// Serialization uses serde's default external tagging: unit variants
12/// serialize as a bare string (`"BrowserBack"`) and the tuple variant
13/// serializes as a single-key table (`{ CustomShortcut = "my chord" }`).
14///
15/// **Stability contract:** existing variant *names* are frozen — they form the
16/// on-disk `config.toml` schema. New variants may be appended freely; removing
17/// or renaming a variant requires a `schema_version` bump and a migration.
18///
19/// This type is pure config data: OS-level event synthesis for each variant
20/// lives in the `openlogi-inject` crate (`openlogi_inject::execute`), keeping
21/// this crate platform- and IO-free.
22#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
23pub enum Action {
24    // ── System ───────────────────────────────────────────────────────────────
25    /// Suppress the input entirely — the button or wheel direction is captured
26    /// but no OS event is synthesised, so the physical input does nothing.
27    None,
28
29    // ── Mouse ────────────────────────────────────────────────────────────────
30    /// Primary mouse button.
31    LeftClick,
32    /// Secondary mouse button.
33    RightClick,
34    /// Middle mouse button (wheel click).
35    MiddleClick,
36    /// Mouse "back" side button (extra button 4). Synthesizes the real mouse
37    /// button event, which browsers and most apps interpret as "navigate back"
38    /// natively — unlike [`Action::BrowserBack`], which sends ⌘[ and is ignored
39    /// by many apps.
40    MouseBack,
41    /// Mouse "forward" side button (extra button 5). Native counterpart to
42    /// [`Action::MouseBack`]; see [`Action::BrowserForward`] for the ⌘] form.
43    MouseForward,
44
45    // ── Editing ──────────────────────────────────────────────────────────────
46    /// Copy the current selection (⌘C / Ctrl+C).
47    Copy,
48    /// Paste from the clipboard (⌘V / Ctrl+V).
49    Paste,
50    /// Cut the current selection (⌘X / Ctrl+X).
51    Cut,
52    /// Undo the last action (⌘Z / Ctrl+Z).
53    Undo,
54    /// Redo the last undone action (⌘⇧Z on macOS / Ctrl+Shift+Z on Linux).
55    ///
56    /// Note: Ctrl+Y is the dominant redo shortcut in LibreOffice and many GTK
57    /// apps. Ctrl+Shift+Z is used here because it mirrors the macOS convention
58    /// and works in GNOME text fields, browsers, and Electron apps. If Ctrl+Y
59    /// coverage is needed, a `CustomShortcut` binding is the escape hatch.
60    Redo,
61    /// Select all content (⌘A / Ctrl+A).
62    SelectAll,
63    /// Open the find / search bar (⌘F / Ctrl+F).
64    Find,
65    /// Save the current document (⌘S / Ctrl+S).
66    Save,
67
68    // ── Browser / Navigation ──────────────────────────────────────────────────
69    /// Navigate backward in browser history.
70    BrowserBack,
71    /// Navigate forward in browser history.
72    BrowserForward,
73    /// Open a new tab (⌘T / Ctrl+T).
74    NewTab,
75    /// Close the current tab (⌘W / Ctrl+W).
76    CloseTab,
77    /// Reopen the last closed tab (⌘⇧T / Ctrl+Shift+T).
78    ReopenTab,
79    /// Switch to the next tab (⌃⇥ / Ctrl+Tab).
80    NextTab,
81    /// Switch to the previous tab (⌃⇧⇥ / Ctrl+Shift+Tab).
82    PrevTab,
83    /// Reload the current page (⌘R / Ctrl+R).
84    ReloadPage,
85
86    // ── Navigation / Window ───────────────────────────────────────────────────
87    /// macOS Mission Control (⌃↑).
88    MissionControl,
89    /// macOS App Exposé — all windows for the current app (⌃↓).
90    AppExpose,
91    /// Switch to the previous desktop / Space.
92    PreviousDesktop,
93    /// Switch to the next desktop / Space.
94    NextDesktop,
95    /// Show the desktop (hide all windows).
96    ShowDesktop,
97    /// Open Launchpad.
98    LaunchpadShow,
99
100    // ── System ────────────────────────────────────────────────────────────────
101    /// Lock the screen (⌘⌃Q on macOS).
102    ///
103    /// On Linux, calls `org.freedesktop.login1.Manager.LockSession($XDG_SESSION_ID)`
104    /// on the system bus (current session only). Falls back to Super+L when
105    /// `$XDG_SESSION_ID` is unset or on non-systemd systems.
106    LockScreen,
107    /// Capture a screenshot.
108    Screenshot,
109    /// Capture a selected screen region to the clipboard.
110    ///
111    /// macOS uses Cmd+Shift+Ctrl+4; Windows uses Win+Shift+S. Linux delegates
112    /// to the desktop environment's screenshot handler via Print Screen.
113    CaptureRegion,
114
115    // ── Media ────────────────────────────────────────────────────────────────
116    /// Toggle media play/pause.
117    PlayPause,
118    /// Skip to the next track.
119    NextTrack,
120    /// Go back to the previous track.
121    PrevTrack,
122    /// Increase system volume.
123    VolumeUp,
124    /// Decrease system volume.
125    VolumeDown,
126    /// Toggle system mute.
127    MuteVolume,
128
129    // ── DPI ──────────────────────────────────────────────────────────────────
130    /// Step through the configured DPI preset list (P1.7).
131    CycleDpiPresets,
132    /// Jump to a specific zero-based preset in the device's DPI preset list.
133    /// Out-of-range indices clamp to the list length at fire time (P1.7).
134    SetDpiPreset(u8),
135    /// Toggle the HID++ SmartShift ratchet/free-spin wheel mode (P1.1).
136    ToggleSmartShift,
137
138    // ── Scroll ───────────────────────────────────────────────────────────────
139    /// Synthesise a vertical scroll-up tick.
140    ScrollUp,
141    /// Synthesise a vertical scroll-down tick.
142    ScrollDown,
143    /// Synthesise a horizontal scroll-left tick.
144    HorizontalScrollLeft,
145    /// Synthesise a horizontal scroll-right tick.
146    HorizontalScrollRight,
147
148    // ── Custom ───────────────────────────────────────────────────────────────
149    /// Replay an arbitrary recorded key chord (P1.3).
150    ///
151    /// Holds the structured chord data so `openlogi_inject::execute` can post the
152    /// real keystroke (macOS: CGEventPost with the encoded modifier flags).
153    /// The `display` field is used by [`Action::label`] so the popover
154    /// shows the user-friendly chord name.
155    CustomShortcut(KeyCombo),
156
157    // ── System (appended) ────────────────────────────────────────────────────
158    /// Put the computer to sleep. Appended after `CustomShortcut` because the
159    /// serde variant index is the wire format (see the stability contract
160    /// above) — new variants only ever go at the end.
161    Sleep,
162    /// Type an arbitrary string by emitting unicode characters (macOS
163    /// `CGEventKeyboardSetUnicodeString`). Used for macro text. Power-user
164    /// escape hatch — excluded from the default catalog.
165    TypeText(String),
166    /// Run an AppleScript via `osascript -e <source>`. Power-user escape hatch.
167    RunAppleScript(String),
168    /// Run a shell command via `/bin/sh -c <command>`. Power-user escape hatch.
169    RunShellCommand(String),
170    /// Run a timed, ordered sequence of steps — the native, no-code version of
171    /// "type 'bite me', wait 5s, press Enter, wait 5s, type more, Escape". Each
172    /// step is one of the power-user actions or a `Delay`. The sequencer
173    /// (`openlogi-inject`) runs them in order, awaiting `Delay`s. Power-user
174    /// escape hatch — excluded from the default catalog.
175    Workflow(Vec<WorkflowStep>),
176    /// Open the configured Actions Ring at the current pointer position.
177    /// The agent handles the ring session rather than the OS injector.
178    ShowActionsRing,
179    /// Open an application, folder, filesystem path, or platform URL.
180    OpenApplication(ApplicationTarget),
181}
182
183/// One step in a [`Action::Workflow`]. A workflow is a `Vec<WorkflowStep>`
184/// executed in order by the inject layer; `Delay` introduces a pause between
185/// the surrounding steps.
186///
187/// `PressKey` reuses [`KeyCombo`] (the same model as [`Action::CustomShortcut`])
188/// so a step can press a key chord. The other variants mirror their standalone
189/// [`Action`] counterparts.
190#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
191pub enum WorkflowStep {
192    /// Type a unicode string (see [`Action::TypeText`]).
193    TypeText(String),
194    /// Press a key chord (see [`Action::CustomShortcut`] / [`KeyCombo`]).
195    PressKey(KeyCombo),
196    /// Wait `millis` milliseconds before the next step.
197    Delay {
198        /// Pause length in milliseconds.
199        millis: u64,
200    },
201    /// Run an AppleScript (see [`Action::RunAppleScript`]).
202    RunAppleScript(String),
203    /// Run a shell command (see [`Action::RunShellCommand`]).
204    RunShellCommand(String),
205}
206
207impl Action {
208    /// Display label for the popover row.
209    ///
210    /// Returns `String` rather than `&str` so parameterized variants (e.g.
211    /// `SetDpiPreset(i)`, `CustomShortcut(s)`) can build a label that
212    /// includes their payload.
213    #[must_use]
214    pub fn label(&self) -> String {
215        match self {
216            Action::None => "Do Nothing".into(),
217            Action::LeftClick => "Left Click".into(),
218            Action::RightClick => "Right Click".into(),
219            Action::MiddleClick => "Middle Click".into(),
220            Action::MouseBack => "Back (Button 4)".into(),
221            Action::MouseForward => "Forward (Button 5)".into(),
222            Action::Copy => "Copy".into(),
223            Action::Paste => "Paste".into(),
224            Action::Cut => "Cut".into(),
225            Action::Undo => "Undo".into(),
226            Action::Redo => "Redo".into(),
227            Action::SelectAll => "Select All".into(),
228            Action::Find => "Find".into(),
229            Action::Save => "Save".into(),
230            Action::BrowserBack => "Browser Back".into(),
231            Action::BrowserForward => "Browser Forward".into(),
232            Action::NewTab => "New Tab".into(),
233            Action::CloseTab => "Close Tab".into(),
234            Action::ReopenTab => "Reopen Tab".into(),
235            Action::NextTab => "Next Tab".into(),
236            Action::PrevTab => "Previous Tab".into(),
237            Action::ReloadPage => "Reload Page".into(),
238            Action::MissionControl => "Mission Control".into(),
239            Action::AppExpose => "App Exposé".into(),
240            Action::PreviousDesktop => "Previous Desktop".into(),
241            Action::NextDesktop => "Next Desktop".into(),
242            Action::ShowDesktop => "Show Desktop".into(),
243            Action::LaunchpadShow => "Launchpad".into(),
244            Action::LockScreen => "Lock Screen".into(),
245            Action::Screenshot => "Screenshot".into(),
246            Action::CaptureRegion => "Capture Region".into(),
247            Action::PlayPause => "Play / Pause".into(),
248            Action::NextTrack => "Next Track".into(),
249            Action::PrevTrack => "Previous Track".into(),
250            Action::VolumeUp => "Volume Up".into(),
251            Action::VolumeDown => "Volume Down".into(),
252            Action::MuteVolume => "Mute".into(),
253            Action::CycleDpiPresets => "Cycle DPI Presets".into(),
254            Action::SetDpiPreset(i) => format!("DPI Preset {}", i + 1),
255            Action::ToggleSmartShift => "Toggle SmartShift".into(),
256            Action::ScrollUp => "Scroll Up".into(),
257            Action::ScrollDown => "Scroll Down".into(),
258            Action::HorizontalScrollLeft => "Scroll Left".into(),
259            Action::HorizontalScrollRight => "Scroll Right".into(),
260            Action::CustomShortcut(combo) => combo.rendered_label(),
261            Action::Sleep => "Sleep".into(),
262            Action::TypeText(s) => format!("Type \"{s}\""),
263            Action::RunAppleScript(_) => "Run AppleScript".into(),
264            Action::RunShellCommand(_) => "Run Command".into(),
265            Action::Workflow(steps) => format!("Workflow ({} steps)", steps.len()),
266            Action::ShowActionsRing => "Actions Ring".into(),
267            Action::OpenApplication(target) => format!("Open {}", target.display_name()),
268        }
269    }
270
271    /// Which [`Category`] this action belongs to, used for popover grouping.
272    #[must_use]
273    pub fn category(&self) -> Category {
274        match self {
275            Action::LeftClick
276            | Action::RightClick
277            | Action::MiddleClick
278            | Action::MouseBack
279            | Action::MouseForward => Category::Mouse,
280            // CustomShortcut is assigned to Editing so it doesn't need a
281            // separate arm (it's not in the picker catalog).
282            Action::Copy
283            | Action::Paste
284            | Action::Cut
285            | Action::Undo
286            | Action::Redo
287            | Action::SelectAll
288            | Action::Find
289            | Action::Save
290            | Action::CustomShortcut(_)
291            | Action::TypeText(_)
292            | Action::RunAppleScript(_)
293            | Action::RunShellCommand(_)
294            | Action::Workflow(_) => Category::Editing,
295            Action::BrowserBack
296            | Action::BrowserForward
297            | Action::NewTab
298            | Action::CloseTab
299            | Action::ReopenTab
300            | Action::NextTab
301            | Action::PrevTab
302            | Action::ReloadPage => Category::Browser,
303            Action::MissionControl
304            | Action::AppExpose
305            | Action::PreviousDesktop
306            | Action::NextDesktop
307            | Action::ShowDesktop
308            | Action::LaunchpadShow => Category::Navigation,
309            Action::None
310            | Action::LockScreen
311            | Action::Screenshot
312            | Action::CaptureRegion
313            | Action::Sleep
314            | Action::ShowActionsRing
315            | Action::OpenApplication(_) => Category::System,
316            Action::PlayPause
317            | Action::NextTrack
318            | Action::PrevTrack
319            | Action::VolumeUp
320            | Action::VolumeDown
321            | Action::MuteVolume => Category::Media,
322            Action::CycleDpiPresets | Action::SetDpiPreset(_) | Action::ToggleSmartShift => {
323                Category::Dpi
324            }
325            Action::ScrollUp
326            | Action::ScrollDown
327            | Action::HorizontalScrollLeft
328            | Action::HorizontalScrollRight => Category::Scroll,
329        }
330    }
331
332    /// All pickable actions in a deterministic order.
333    ///
334    /// [`Action::CustomShortcut`] is intentionally excluded — it is opened via
335    /// "Record shortcut…" (P1.3), not selected from the catalog.
336    #[must_use]
337    pub fn catalog() -> Vec<Action> {
338        vec![
339            // Mouse
340            Action::LeftClick,
341            Action::RightClick,
342            Action::MiddleClick,
343            Action::MouseBack,
344            Action::MouseForward,
345            // Editing
346            Action::Copy,
347            Action::Paste,
348            Action::Cut,
349            Action::Undo,
350            Action::Redo,
351            Action::SelectAll,
352            Action::Find,
353            Action::Save,
354            // Browser
355            Action::BrowserBack,
356            Action::BrowserForward,
357            Action::NewTab,
358            Action::CloseTab,
359            Action::ReopenTab,
360            Action::NextTab,
361            Action::PrevTab,
362            Action::ReloadPage,
363            // Navigation
364            Action::MissionControl,
365            Action::AppExpose,
366            Action::PreviousDesktop,
367            Action::NextDesktop,
368            Action::ShowDesktop,
369            Action::LaunchpadShow,
370            // System
371            Action::None,
372            Action::LockScreen,
373            Action::Screenshot,
374            Action::CaptureRegion,
375            Action::Sleep,
376            // Media
377            Action::PlayPause,
378            Action::NextTrack,
379            Action::PrevTrack,
380            Action::VolumeUp,
381            Action::VolumeDown,
382            Action::MuteVolume,
383            // DPI
384            Action::CycleDpiPresets,
385            Action::ToggleSmartShift,
386            // Scroll
387            Action::ScrollUp,
388            Action::ScrollDown,
389            Action::HorizontalScrollLeft,
390            Action::HorizontalScrollRight,
391        ]
392    }
393}