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}