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