buffr_modal/actions.rs
1//! Page-mode action enum + mode states.
2//!
3//! [`PageAction`] is what the keymap dispatcher emits. The host (CEF
4//! shell) translates each into a CEF command. Variants here include
5//! both nullary actions (`Reload`, `TabClose`) and count-bearing
6//! scrolls (`ScrollDown(u32)`).
7//!
8//! Mode-transition actions (`OpenOmnibar`, `EnterHintMode`,
9//! `EnterInsertMode`, `EnterMode`) are emitted to the host *and* drive
10//! [`crate::engine::Engine::set_mode`] at the same point — see the
11//! design note at the top of `engine.rs`.
12
13use serde::{Deserialize, Serialize};
14
15/// Coarse mode displayed in the status line. `Insert` is a single state
16/// here even though `hjkl_engine` may be in Normal/Insert/Visual
17/// internally — the page-mode FSM doesn't care which sub-mode the
18/// embedded editor is in, only that page-level keystrokes route to
19/// `BuffrHost` instead of the page action dispatcher.
20#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
21pub enum Mode {
22 #[default]
23 Normal,
24 Visual,
25 Command,
26 Hint,
27 Insert,
28}
29
30/// Page-mode FSM states. Distinct from [`Mode`] (the status-line
31/// summary) — `PageMode` is what the keymap trie dispatches against.
32#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
33#[serde(rename_all = "snake_case")]
34pub enum PageMode {
35 #[default]
36 Normal,
37 Visual,
38 Command,
39 Hint,
40 /// A pending key sequence is being collected (e.g., `g…`,
41 /// `<C-w>…`). Internal — surfaces from `Engine::mode()` only while
42 /// a multi-chord prefix is mid-flight.
43 Pending,
44 /// Insert-mode is active; keystrokes route through
45 /// [`crate::engine::Engine::feed_edit_mode_key`] which (post-Phase
46 /// 2) hands off to `hjkl_editor::Editor`.
47 Insert,
48}
49
50/// Page-level actions emitted by the modal dispatcher.
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "snake_case")]
53pub enum PageAction {
54 // -- scroll --------------------------------------------------------
55 ScrollUp(u32),
56 ScrollDown(u32),
57 ScrollLeft(u32),
58 ScrollRight(u32),
59 ScrollPageUp,
60 ScrollPageDown,
61 /// `<C-f>` — full-window forward scroll.
62 ScrollFullPageDown,
63 /// `<C-b>` — full-window back scroll.
64 ScrollFullPageUp,
65 ScrollHalfPageDown,
66 ScrollHalfPageUp,
67 ScrollTop,
68 ScrollBottom,
69
70 // -- tabs ---------------------------------------------------------
71 TabNext,
72 TabPrev,
73 TabClose,
74 TabNew,
75 /// Open a fresh tab adjacent to the active tab. The apps layer also
76 /// auto-opens the omnibar after creation so the user can type a URL.
77 /// Inserts the new tab immediately to the right of the active tab.
78 TabNewRight,
79 /// Open a fresh tab adjacent to the active tab. The apps layer also
80 /// auto-opens the omnibar after creation so the user can type a URL.
81 /// Inserts the new tab immediately to the left of the active tab.
82 TabNewLeft,
83 /// Pin / unpin the active tab. Toggle — pinned tabs sort first;
84 /// pin does **not** prevent close. Default keybind `<leader>p`.
85 PinTab,
86 /// Re-open the most recently closed tab. Repeated invocation
87 /// pops further entries from the close stack so successive
88 /// closes can be undone in reverse order. Default keybind `u`.
89 ReopenClosedTab,
90 /// Paste a URL from the clipboard into a new tab adjacent to the
91 /// active one. The clipboard text must classify as Url or Host
92 /// (per buffr-config's `classify_input`); other content is a
93 /// no-op. `after = true` inserts to the right (`p`), `false` to
94 /// the left (`P`).
95 PasteUrl {
96 after: bool,
97 },
98 /// Reorder the tab list. Currently unbound; reserved for the
99 /// eventual mouse-drag handler.
100 TabReorder {
101 from: u32,
102 to: u32,
103 },
104 /// Shuffle the active tab one slot left in the strip. Default
105 /// keybind `<C-S-h>`; clamps at index 0.
106 MoveTabLeft,
107 /// Shuffle the active tab one slot right in the strip. Default
108 /// keybind `<C-S-l>`; clamps at the last index.
109 MoveTabRight,
110
111 // -- history ------------------------------------------------------
112 HistoryBack,
113 HistoryForward,
114 Reload,
115 /// Hard reload bypassing cache (`<C-r>`).
116 ReloadHard,
117 StopLoading,
118
119 // -- mode transitions --------------------------------------------
120 OpenOmnibar,
121 OpenCommandLine,
122 EnterHintMode,
123 /// Background-tab variant of hint mode (`F` in vimium).
124 EnterHintModeBackground,
125 /// Generic mode-transition variant. Equivalent to the more
126 /// specific `OpenOmnibar`/`OpenCommandLine`/`EnterHintMode`/
127 /// `EnterInsertMode` actions but parameterised — useful for user
128 /// config that wants `<F2>` → command mode.
129 EnterMode(PageMode),
130
131 // -- find ---------------------------------------------------------
132 /// `/` (forward) or `?` (backward).
133 Find {
134 forward: bool,
135 },
136 FindNext,
137 FindPrev,
138
139 // -- yank ---------------------------------------------------------
140 /// Yank the current page URL. Phase 2 emits the action; clipboard
141 /// plumbing lands with the host wiring in `buffr-core`.
142 YankUrl,
143 /// Yank the active text selection in the page to the system
144 /// clipboard. Bound to `y` in Visual mode. After the copy, the
145 /// apps layer transitions back to Normal.
146 YankSelection,
147
148 // -- zoom ---------------------------------------------------------
149 ZoomIn,
150 ZoomOut,
151 ZoomReset,
152
153 // -- devtools / misc ---------------------------------------------
154 OpenDevTools,
155
156 // -- downloads ----------------------------------------------------
157 /// Delete every `Completed` download row. Does not have a default
158 /// keybinding — there's no obvious vim-flavored chord — so it's
159 /// reachable only via user config (`[keymap.normal] "..." =
160 /// "clear_completed_downloads"`) or the eventual `:downloads`
161 /// command line in Phase 3 chrome work.
162 ClearCompletedDownloads,
163
164 /// Defer to the embedded `hjkl_engine::Editor`. Keystroke
165 /// unchanged; the modal dispatcher swallows nothing on this path.
166 EnterInsertMode,
167
168 /// Focus the first text input on the page and enter insert mode.
169 /// Vieb's `gi` / `insertAtFirstInput`.
170 FocusFirstInput,
171
172 /// Exit insert mode unconditionally — blurs the focused DOM element
173 /// and returns the engine to PageMode::Normal.
174 ExitInsertMode,
175}
176
177impl PageAction {
178 /// Whether the action is safe to fire from an OS auto-repeat
179 /// keystroke. Idempotent or stream-friendly actions (scrolls, tab
180 /// cycling, history nav, find-next, zoom) return `true`; one-shot
181 /// state mutations (close tab, paste, mode change, devtools) return
182 /// `false` so holding the key doesn't spam them.
183 pub fn is_repeatable(&self) -> bool {
184 use PageAction::*;
185 matches!(
186 self,
187 ScrollUp(_)
188 | ScrollDown(_)
189 | ScrollLeft(_)
190 | ScrollRight(_)
191 | ScrollPageUp
192 | ScrollPageDown
193 | ScrollFullPageDown
194 | ScrollFullPageUp
195 | ScrollHalfPageDown
196 | ScrollHalfPageUp
197 | TabNext
198 | TabPrev
199 | MoveTabLeft
200 | MoveTabRight
201 | HistoryBack
202 | HistoryForward
203 | FindNext
204 | FindPrev
205 | ZoomIn
206 | ZoomOut
207 )
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn page_mode_serde_round_trip() {
217 let s = toml::to_string(&Wrap {
218 m: PageMode::Visual,
219 })
220 .unwrap();
221 let back: Wrap = toml::from_str(&s).unwrap();
222 assert_eq!(back.m, PageMode::Visual);
223 }
224
225 #[test]
226 fn page_action_serde_round_trip_unit() {
227 let s = toml::to_string(&Wrap2 {
228 a: PageAction::Reload,
229 })
230 .unwrap();
231 let back: Wrap2 = toml::from_str(&s).unwrap();
232 assert_eq!(back.a, PageAction::Reload);
233 }
234
235 #[test]
236 fn page_action_serde_round_trip_count() {
237 let s = toml::to_string(&Wrap2 {
238 a: PageAction::ScrollDown(5),
239 })
240 .unwrap();
241 let back: Wrap2 = toml::from_str(&s).unwrap();
242 assert_eq!(back.a, PageAction::ScrollDown(5));
243 }
244
245 #[derive(Serialize, Deserialize)]
246 struct Wrap {
247 m: PageMode,
248 }
249
250 #[derive(Serialize, Deserialize)]
251 struct Wrap2 {
252 a: PageAction,
253 }
254}