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 { action: Some(action), keys: keys.into_iter().map(Keystroke::plain).collect(), intent }
34    }
35
36    /// Binds keys to an intention no menu action gates.
37    fn ungated(keys: impl IntoIterator<Item = Key>, intent: ManagerIntentOp<Id, Source, Format, Change>) -> Self {
38        Self { action: None, keys: keys.into_iter().map(Keystroke::plain).collect(), intent }
39    }
40
41    /// Observes whether one keystroke reaches the binding.
42    fn binds(&self, stroke: Keystroke) -> bool {
43        self.keys.contains(&stroke)
44    }
45}
46
47/// States the keys one page binds and what they denote there.
48#[ext(name = KeyBindingExt)]
49pub impl<This> This
50where
51    This: NavigationStateAlg,
52{
53    /// States every keystroke the current page binds, cursor movement first.
54    ///
55    /// Typed scalars are not bound here: on an editor page every scalar the table leaves free
56    /// denotes insertion, which is one rule rather than one binding per character.
57    fn page_bindings(&self) -> impl Iterator<Item = ManagerKeyBinding<This>> {
58        let bindings = match self.page() {
59            ManagerPage::Collection => vec![
60                KeyBinding::action(
61                    ManagerAction::Previous,
62                    [Key::Up, Key::Character('k')],
63                    ManagerIntentOp::SelectPrevious {},
64                ),
65                KeyBinding::action(
66                    ManagerAction::Next,
67                    [Key::Down, Key::Character('j')],
68                    ManagerIntentOp::SelectNext {},
69                ),
70                KeyBinding::action(ManagerAction::Activate, [Key::Enter], ManagerIntentOp::OpenSelected {}),
71                KeyBinding::action(
72                    ManagerAction::AddSources,
73                    [Key::Character('a')],
74                    ManagerIntentOp::OpenAddSources {},
75                ),
76                KeyBinding::action(ManagerAction::Space, [Key::Character(' ')], ManagerIntentOp::ApplySelectedSpace {}),
77                KeyBinding::action(ManagerAction::Remove, [Key::Delete], ManagerIntentOp::RemoveSelected {}),
78            ],
79            ManagerPage::Details(_) => vec![
80                KeyBinding::action(
81                    ManagerAction::Previous,
82                    [Key::Up, Key::Character('k')],
83                    ManagerIntentOp::SelectPreviousDetail {},
84                ),
85                KeyBinding::action(
86                    ManagerAction::Next,
87                    [Key::Down, Key::Character('j')],
88                    ManagerIntentOp::SelectNextDetail {},
89                ),
90                KeyBinding::action(ManagerAction::Activate, [Key::Enter], ManagerIntentOp::ActivateDetail {}),
91                KeyBinding::action(ManagerAction::Space, [Key::Character(' ')], ManagerIntentOp::ApplySelectedSpace {}),
92                KeyBinding::action(ManagerAction::Back, [Key::Escape, Key::Backspace], ManagerIntentOp::Back {}),
93            ],
94            ManagerPage::Formats(_) => vec![
95                KeyBinding::action(
96                    ManagerAction::Previous,
97                    [Key::Up, Key::Character('k')],
98                    ManagerIntentOp::SelectPreviousFormat {},
99                ),
100                KeyBinding::action(
101                    ManagerAction::Next,
102                    [Key::Down, Key::Character('j')],
103                    ManagerIntentOp::SelectNextFormat {},
104                ),
105                // Accepting a choice already applied leaves the page it was chosen on.
106                KeyBinding::action(ManagerAction::Activate, [Key::Enter], ManagerIntentOp::Back {}),
107                KeyBinding::action(ManagerAction::Back, [Key::Escape, Key::Backspace], ManagerIntentOp::Back {}),
108            ],
109            ManagerPage::Log(_) | ManagerPage::Report(_) | ManagerPage::Command(_) => {
110                vec![KeyBinding::action(ManagerAction::Back, [Key::Escape, Key::Enter], ManagerIntentOp::Back {})]
111            }
112            ManagerPage::AddSources => editor_bindings(ManagerIntentOp::SubmitDraft {}),
113            ManagerPage::Output(_) => editor_bindings(ManagerIntentOp::SubmitOutput {}),
114            ManagerPage::Input(_) => editor_bindings(ManagerIntentOp::SubmitInput {}),
115        };
116        bindings.into_iter()
117    }
118
119    /// States what one keystroke denotes on the current page.
120    ///
121    /// Exit is bound on every page. Any other modified keystroke denotes nothing, so a chord the
122    /// terminal reports is never mistaken for the unmodified key inside it.
123    fn keystroke_meaning(&self, stroke: Keystroke) -> Option<ManagerKeyBinding<This>> {
124        if stroke == EXIT_KEYSTROKE {
125            return Some(KeyBinding::action(
126                ManagerAction::Exit,
127                [EXIT_KEYSTROKE.key],
128                ManagerIntentOp::SafeExitRequested {},
129            ));
130        }
131        if stroke.modified() {
132            return None;
133        }
134        self.page_bindings().find(|binding| binding.binds(stroke)).or_else(|| self.typed_meaning(stroke.key.typed()?))
135    }
136
137    /// States what one typed scalar denotes on the current page.
138    fn typed_meaning(&self, typed: char) -> Option<ManagerKeyBinding<This>> {
139        self.page_accepts_text().then(|| {
140            KeyBinding::ungated([Key::Character(typed)], ManagerIntentOp::InsertText { text: typed.to_string() })
141        })
142    }
143
144    /// States what pasted text denotes on the current page.
145    fn paste_meaning(&self, text: &str) -> Option<ManagerKeyBinding<This>> {
146        self.page_accepts_text().then(|| KeyBinding::ungated([], ManagerIntentOp::InsertText { text: text.to_owned() }))
147    }
148
149    /// Observes whether the current page holds a text draft free scalars belong to.
150    fn page_accepts_text(&self) -> bool {
151        matches!(self.page(), ManagerPage::AddSources | ManagerPage::Output(_) | ManagerPage::Input(_))
152    }
153
154    /// States the keys reaching one action on the current page, the primary key first.
155    fn action_keys(&self, action: ManagerAction) -> impl Iterator<Item = Keystroke> {
156        if action == ManagerAction::Exit {
157            return vec![EXIT_KEYSTROKE].into_iter();
158        }
159        self.page_bindings()
160            .find(|binding| binding.action == Some(action))
161            .map(|binding| binding.keys)
162            .unwrap_or_default()
163            .into_iter()
164    }
165}
166
167/// Interprets input as the intentions it denotes.
168#[ext(name = KeyInputExt)]
169pub impl<This> This
170where
171    This: NavigationStateAlg
172        + QueueCatalogAlg
173        + QueueAppendAlg
174        + QueueRemoveAlg
175        + QueuePauseAlg
176        + QueueDuplicateAlg
177        + QueueFormatEditAlg
178        + FormatCatalogStateAlg
179        + QueueDryRunAlg
180        + RehearsalStateAlg
181        + TransferStateAlg
182        + DetailSelectionAlg
183        + DraftStateAlg
184        + OutputDraftAlg
185        + InputDraftAlg
186        + QueueOutputAlg
187        + QueueSourceAlg
188        + SourceMetadataAlg
189        + TextEditorStateAlg
190        + ManagerStatusAlg
191        + SafeExitAlg
192        + SourceRequestAlg
193        + SubmissionAlg
194        + MediaOptionsAlg
195        + OutputChoiceAlg,
196    This::Entry: QueueEntryAlg + RequestOptionsAlg,
197    This::Id: Copy + Eq,
198{
199    /// Applies what one keystroke denotes, and observes whether it denoted anything.
200    ///
201    /// A gated binding applies only while its action has meaning, so a disabled menu entry and a
202    /// pressed key refuse the same intention.
203    fn apply_keystroke(&mut self, stroke: Keystroke) -> bool {
204        self.keystroke_meaning(stroke).is_some_and(|binding| self.apply_binding(binding))
205    }
206
207    /// Applies what pasted text denotes, and observes whether it denoted anything.
208    fn apply_paste(&mut self, text: &str) -> bool {
209        self.paste_meaning(text).is_some_and(|binding| self.apply_binding(binding))
210    }
211
212    /// Applies one binding while the action gating it has meaning.
213    fn apply_binding(&mut self, binding: ManagerKeyBinding<This>) -> bool {
214        let enabled = binding.action.is_none_or(|action| self.action_availability(action).is_enabled());
215        if enabled {
216            self.apply_manager_event(binding.intent);
217        }
218        enabled
219    }
220}
221
222/// States the keys every text editor binds, and the intention submitting its draft.
223fn editor_bindings<Id, Source, Format, Change>(
224    submit: ManagerIntentOp<Id, Source, Format, Change>,
225) -> Vec<KeyBinding<Id, Source, Format, Change>> {
226    vec![
227        KeyBinding::action(ManagerAction::Previous, [Key::Left], ManagerIntentOp::MoveCursorLeft {}),
228        KeyBinding::action(ManagerAction::Next, [Key::Right], ManagerIntentOp::MoveCursorRight {}),
229        KeyBinding::action(ManagerAction::Activate, [Key::Enter], submit),
230        KeyBinding::action(ManagerAction::Back, [Key::Escape], ManagerIntentOp::Back {}),
231        KeyBinding::ungated([Key::Backspace], ManagerIntentOp::DeleteBeforeCursor {}),
232        KeyBinding::ungated([Key::Delete], ManagerIntentOp::DeleteAtCursor {}),
233        KeyBinding::ungated([Key::Home], ManagerIntentOp::MoveCursorHome {}),
234        KeyBinding::ungated([Key::End], ManagerIntentOp::MoveCursorEnd {}),
235    ]
236}