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}