Skip to main content

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}