Skip to main content

rsynko_ui/
binding.rs

1use crate::*;
2use alux_ext::ext;
3use rsynko_manager::*;
4
5/// Names the binding one manager's pages state, whatever that manager carries.
6pub type ManagerKeyBinding<Manager> = KeyBinding<
7    <Manager as ManagerSorts>::Id,
8    <Manager as ManagerSorts>::Source,
9    <Manager as ManagerSorts>::Format,
10    <Manager as ManagerSorts>::Change,
11>;
12
13/// Binds keystrokes on one page to the intention they denote.
14///
15/// A binding states its keys, the menu action they count as, and the intention itself. An
16/// intention with no action is ungated: text editing means what it means without a menu entry.
17pub struct KeyBinding<Id, Source, Format, Change> {
18    /// Names the menu action the binding counts as, when it counts as one.
19    pub action: Option<ManagerAction>,
20    /// Names every key reaching the binding, the one a menu states first.
21    pub keys: Vec<Keystroke>,
22    /// Denotes the intention the keys carry.
23    pub intent: ManagerIntentOp<Id, Source, Format, Change>,
24}
25
26impl<Id, Source, Format, Change> KeyBinding<Id, Source, Format, Change> {
27    /// Binds keys to one menu action and the intention that action denotes.
28    fn action(
29        action: ManagerAction,
30        keys: impl IntoIterator<Item = Key>,
31        intent: ManagerIntentOp<Id, Source, Format, Change>,
32    ) -> Self {
33        Self {
34            action: Some(action),
35            keys: keys.into_iter().map(Keystroke::plain).collect(),
36            intent,
37        }
38    }
39
40    /// Binds keys to an intention no menu action gates.
41    fn ungated(
42        keys: impl IntoIterator<Item = Key>,
43        intent: ManagerIntentOp<Id, Source, Format, Change>,
44    ) -> Self {
45        Self {
46            action: None,
47            keys: keys.into_iter().map(Keystroke::plain).collect(),
48            intent,
49        }
50    }
51
52    /// Observes whether one keystroke reaches the binding.
53    fn binds(&self, stroke: Keystroke) -> bool {
54        self.keys.contains(&stroke)
55    }
56}
57
58/// States the keys one page binds and what they denote there.
59#[ext(name = KeyBindingExt)]
60pub impl<This> This
61where
62    This: NavigationStateAlg,
63{
64    /// States every keystroke the current page binds, cursor movement first.
65    ///
66    /// Typed scalars are not bound here: on an editor page every scalar the table leaves free
67    /// denotes insertion, which is one rule rather than one binding per character.
68    fn page_bindings(&self) -> impl Iterator<Item = ManagerKeyBinding<This>> {
69        let bindings = match self.page() {
70            ManagerPage::Collection => vec![
71                KeyBinding::action(
72                    ManagerAction::Previous,
73                    [Key::Up, Key::Character('k')],
74                    ManagerIntentOp::SelectPrevious {},
75                ),
76                KeyBinding::action(
77                    ManagerAction::Next,
78                    [Key::Down, Key::Character('j')],
79                    ManagerIntentOp::SelectNext {},
80                ),
81                KeyBinding::action(
82                    ManagerAction::Activate,
83                    [Key::Enter],
84                    ManagerIntentOp::OpenSelected {},
85                ),
86                KeyBinding::action(
87                    ManagerAction::AddSources,
88                    [Key::Character('a')],
89                    ManagerIntentOp::OpenAddSources {},
90                ),
91                KeyBinding::action(
92                    ManagerAction::Space,
93                    [Key::Character(' ')],
94                    ManagerIntentOp::ApplySelectedSpace {},
95                ),
96                KeyBinding::action(
97                    ManagerAction::Remove,
98                    [Key::Delete],
99                    ManagerIntentOp::RemoveSelected {},
100                ),
101            ],
102            ManagerPage::Details(_) => vec![
103                KeyBinding::action(
104                    ManagerAction::Previous,
105                    [Key::Up, Key::Character('k')],
106                    ManagerIntentOp::SelectPreviousDetail {},
107                ),
108                KeyBinding::action(
109                    ManagerAction::Next,
110                    [Key::Down, Key::Character('j')],
111                    ManagerIntentOp::SelectNextDetail {},
112                ),
113                KeyBinding::action(
114                    ManagerAction::Activate,
115                    [Key::Enter],
116                    ManagerIntentOp::ActivateDetail {},
117                ),
118                KeyBinding::action(
119                    ManagerAction::Space,
120                    [Key::Character(' ')],
121                    ManagerIntentOp::ApplySelectedSpace {},
122                ),
123                KeyBinding::action(
124                    ManagerAction::Back,
125                    [Key::Escape, Key::Backspace],
126                    ManagerIntentOp::Back {},
127                ),
128            ],
129            ManagerPage::Formats(_) => vec![
130                KeyBinding::action(
131                    ManagerAction::Previous,
132                    [Key::Up, Key::Character('k')],
133                    ManagerIntentOp::SelectPreviousFormat {},
134                ),
135                KeyBinding::action(
136                    ManagerAction::Next,
137                    [Key::Down, Key::Character('j')],
138                    ManagerIntentOp::SelectNextFormat {},
139                ),
140                // Accepting a choice already applied leaves the page it was chosen on.
141                KeyBinding::action(
142                    ManagerAction::Activate,
143                    [Key::Enter],
144                    ManagerIntentOp::Back {},
145                ),
146                KeyBinding::action(
147                    ManagerAction::Back,
148                    [Key::Escape, Key::Backspace],
149                    ManagerIntentOp::Back {},
150                ),
151            ],
152            ManagerPage::Log(_) | ManagerPage::Report(_) | ManagerPage::Command(_) => {
153                vec![KeyBinding::action(
154                    ManagerAction::Back,
155                    [Key::Escape, Key::Enter],
156                    ManagerIntentOp::Back {},
157                )]
158            }
159            ManagerPage::AddSources => editor_bindings(ManagerIntentOp::SubmitDraft {}),
160            ManagerPage::Output(_) => editor_bindings(ManagerIntentOp::SubmitOutput {}),
161            ManagerPage::Input(_) => editor_bindings(ManagerIntentOp::SubmitInput {}),
162        };
163        bindings.into_iter()
164    }
165
166    /// States what one keystroke denotes on the current page.
167    ///
168    /// Exit is bound on every page. Any other modified keystroke denotes nothing, so a chord the
169    /// terminal reports is never mistaken for the unmodified key inside it.
170    fn keystroke_meaning(&self, stroke: Keystroke) -> Option<ManagerKeyBinding<This>> {
171        if stroke == EXIT_KEYSTROKE {
172            return Some(KeyBinding::action(
173                ManagerAction::Exit,
174                [EXIT_KEYSTROKE.key],
175                ManagerIntentOp::SafeExitRequested {},
176            ));
177        }
178        if stroke.modified() {
179            return None;
180        }
181        self.page_bindings()
182            .find(|binding| binding.binds(stroke))
183            .or_else(|| self.typed_meaning(stroke.key.typed()?))
184    }
185
186    /// States what one typed scalar denotes on the current page.
187    fn typed_meaning(&self, typed: char) -> Option<ManagerKeyBinding<This>> {
188        self.page_accepts_text().then(|| {
189            KeyBinding::ungated(
190                [Key::Character(typed)],
191                ManagerIntentOp::InsertText {
192                    text: typed.to_string(),
193                },
194            )
195        })
196    }
197
198    /// States what pasted text denotes on the current page.
199    fn paste_meaning(&self, text: &str) -> Option<ManagerKeyBinding<This>> {
200        self.page_accepts_text().then(|| {
201            KeyBinding::ungated(
202                [],
203                ManagerIntentOp::InsertText {
204                    text: text.to_owned(),
205                },
206            )
207        })
208    }
209
210    /// Observes whether the current page holds a text draft free scalars belong to.
211    fn page_accepts_text(&self) -> bool {
212        matches!(
213            self.page(),
214            ManagerPage::AddSources | ManagerPage::Output(_) | ManagerPage::Input(_)
215        )
216    }
217
218    /// States the keys reaching one action on the current page, the primary key first.
219    fn action_keys(&self, action: ManagerAction) -> impl Iterator<Item = Keystroke> {
220        if action == ManagerAction::Exit {
221            return vec![EXIT_KEYSTROKE].into_iter();
222        }
223        self.page_bindings()
224            .find(|binding| binding.action == Some(action))
225            .map(|binding| binding.keys)
226            .unwrap_or_default()
227            .into_iter()
228    }
229}
230
231/// Interprets input as the intentions it denotes.
232#[ext(name = KeyInputExt)]
233pub impl<This> This
234where
235    This: NavigationStateAlg
236        + QueueCatalogAlg
237        + QueueAppendAlg
238        + QueueRemoveAlg
239        + QueuePauseAlg
240        + QueueDuplicateAlg
241        + QueueFormatEditAlg
242        + FormatCatalogStateAlg
243        + QueueDryRunAlg
244        + RehearsalStateAlg
245        + TransferStateAlg
246        + DetailSelectionAlg
247        + DraftStateAlg
248        + OutputDraftAlg
249        + InputDraftAlg
250        + QueueOutputAlg
251        + QueueSourceAlg
252        + SourceMetadataAlg
253        + TextEditorStateAlg
254        + ManagerStatusAlg
255        + SafeExitAlg
256        + SourceRequestAlg
257        + SubmissionAlg
258        + MediaOptionsAlg
259        + OutputChoiceAlg,
260    This::Entry: QueueEntryAlg + RequestOptionsAlg,
261    This::Id: Copy + Eq,
262{
263    /// Applies what one keystroke denotes, and observes whether it denoted anything.
264    ///
265    /// A gated binding applies only while its action has meaning, so a disabled menu entry and a
266    /// pressed key refuse the same intention.
267    fn apply_keystroke(&mut self, stroke: Keystroke) -> bool {
268        self.keystroke_meaning(stroke)
269            .is_some_and(|binding| self.apply_binding(binding))
270    }
271
272    /// Applies what pasted text denotes, and observes whether it denoted anything.
273    fn apply_paste(&mut self, text: &str) -> bool {
274        self.paste_meaning(text)
275            .is_some_and(|binding| self.apply_binding(binding))
276    }
277
278    /// Applies one binding while the action gating it has meaning.
279    fn apply_binding(&mut self, binding: ManagerKeyBinding<This>) -> bool {
280        let enabled = binding
281            .action
282            .is_none_or(|action| self.action_availability(action).is_enabled());
283        if enabled {
284            self.apply_manager_event(binding.intent);
285        }
286        enabled
287    }
288}
289
290/// States the keys every text editor binds, and the intention submitting its draft.
291fn editor_bindings<Id, Source, Format, Change>(
292    submit: ManagerIntentOp<Id, Source, Format, Change>,
293) -> Vec<KeyBinding<Id, Source, Format, Change>> {
294    vec![
295        KeyBinding::action(
296            ManagerAction::Previous,
297            [Key::Left],
298            ManagerIntentOp::MoveCursorLeft {},
299        ),
300        KeyBinding::action(
301            ManagerAction::Next,
302            [Key::Right],
303            ManagerIntentOp::MoveCursorRight {},
304        ),
305        KeyBinding::action(ManagerAction::Activate, [Key::Enter], submit),
306        KeyBinding::action(ManagerAction::Back, [Key::Escape], ManagerIntentOp::Back {}),
307        KeyBinding::ungated([Key::Backspace], ManagerIntentOp::DeleteBeforeCursor {}),
308        KeyBinding::ungated([Key::Delete], ManagerIntentOp::DeleteAtCursor {}),
309        KeyBinding::ungated([Key::Home], ManagerIntentOp::MoveCursorHome {}),
310        KeyBinding::ungated([Key::End], ManagerIntentOp::MoveCursorEnd {}),
311    ]
312}