sabiql 1.10.0

A fast, driver-less TUI for browsing and editing PostgreSQL databases
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
use crate::app::model::app_state::AppState;
use crate::app::model::shared::focused_pane::FocusedPane;
use crate::app::model::shared::ui_state::ResultNavMode;
use crate::app::update::action::{
    Action, CursorPosition, ScrollAmount, ScrollDirection, ScrollTarget, ScrollToCursorTarget,
    SelectMotion,
};
use crate::app::update::input::keybindings::{Key, KeyCombo};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NavigationContext {
    Explorer,
    Inspector,
    ResultScroll,
    ResultRowActive,
    ResultCellActive,
}

impl NavigationContext {
    pub fn is_result(self) -> bool {
        matches!(
            self,
            Self::ResultScroll | Self::ResultRowActive | Self::ResultCellActive
        )
    }

    pub fn is_inspector(self) -> bool {
        self == Self::Inspector
    }

    pub fn from_state(state: &AppState) -> Self {
        let result_nav = state.ui.is_focus_mode() || state.ui.focused_pane == FocusedPane::Result;
        if result_nav {
            match state.result_interaction.selection().mode() {
                ResultNavMode::CellActive => Self::ResultCellActive,
                ResultNavMode::RowActive => Self::ResultRowActive,
                ResultNavMode::Scroll => Self::ResultScroll,
            }
        } else if state.ui.focused_pane == FocusedPane::Inspector {
            Self::Inspector
        } else {
            Self::Explorer
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NavIntent {
    MoveDown,
    MoveUp,
    MoveToFirst,
    MoveToLast,
    ViewportTop,
    ViewportMiddle,
    ViewportBottom,
    MoveLeft,
    MoveRight,
    HalfPageDown,
    HalfPageUp,
    FullPageDown,
    FullPageUp,
    ScrollCursorCenter,
    ScrollCursorTop,
    ScrollCursorBottom,
}

pub fn map_nav_intent(combo: &KeyCombo) -> Option<NavIntent> {
    if combo.modifiers.alt || combo.modifiers.shift {
        return None;
    }

    if combo.modifiers.ctrl {
        return match combo.key {
            Key::Char('n') => Some(NavIntent::MoveDown),
            Key::Char('p') => Some(NavIntent::MoveUp),
            Key::Char('d') => Some(NavIntent::HalfPageDown),
            Key::Char('u') => Some(NavIntent::HalfPageUp),
            Key::Char('f') => Some(NavIntent::FullPageDown),
            Key::Char('b') => Some(NavIntent::FullPageUp),
            _ => None,
        };
    }

    match combo.key {
        Key::Char('j') | Key::Down => Some(NavIntent::MoveDown),
        Key::Char('k') | Key::Up => Some(NavIntent::MoveUp),
        Key::Char('g') | Key::Home => Some(NavIntent::MoveToFirst),
        Key::Char('G') | Key::End => Some(NavIntent::MoveToLast),
        Key::Char('H') => Some(NavIntent::ViewportTop),
        Key::Char('M') => Some(NavIntent::ViewportMiddle),
        Key::Char('L') => Some(NavIntent::ViewportBottom),
        Key::Char('h') | Key::Left => Some(NavIntent::MoveLeft),
        Key::Char('l') | Key::Right => Some(NavIntent::MoveRight),
        Key::PageDown => Some(NavIntent::FullPageDown),
        Key::PageUp => Some(NavIntent::FullPageUp),
        _ => None,
    }
}

pub fn resolve(intent: NavIntent, ctx: NavigationContext) -> Action {
    use NavIntent::{
        FullPageDown, FullPageUp, HalfPageDown, HalfPageUp, MoveDown, MoveLeft, MoveRight,
        MoveToFirst, MoveToLast, MoveUp, ScrollCursorBottom, ScrollCursorCenter, ScrollCursorTop,
        ViewportBottom, ViewportMiddle, ViewportTop,
    };
    use NavigationContext::{Explorer, Inspector, ResultCellActive, ResultRowActive, ResultScroll};

    match (intent, ctx) {
        // MoveDown
        (MoveDown, Explorer) => Action::Select(SelectMotion::Next),
        (MoveDown, Inspector) => Action::Scroll {
            target: ScrollTarget::Inspector,
            direction: ScrollDirection::Down,
            amount: ScrollAmount::Line,
        },
        (MoveDown, ResultScroll | ResultRowActive | ResultCellActive) => Action::Scroll {
            target: ScrollTarget::Result,
            direction: ScrollDirection::Down,
            amount: ScrollAmount::Line,
        },

        // MoveUp
        (MoveUp, Explorer) => Action::Select(SelectMotion::Previous),
        (MoveUp, Inspector) => Action::Scroll {
            target: ScrollTarget::Inspector,
            direction: ScrollDirection::Up,
            amount: ScrollAmount::Line,
        },
        (MoveUp, ResultScroll | ResultRowActive | ResultCellActive) => Action::Scroll {
            target: ScrollTarget::Result,
            direction: ScrollDirection::Up,
            amount: ScrollAmount::Line,
        },

        // MoveToFirst
        (MoveToFirst, Explorer) => Action::Select(SelectMotion::First),
        (MoveToFirst, Inspector) => Action::Scroll {
            target: ScrollTarget::Inspector,
            direction: ScrollDirection::Up,
            amount: ScrollAmount::ToStart,
        },
        (MoveToFirst, ResultScroll | ResultRowActive | ResultCellActive) => Action::Scroll {
            target: ScrollTarget::Result,
            direction: ScrollDirection::Up,
            amount: ScrollAmount::ToStart,
        },

        // MoveToLast
        (MoveToLast, Explorer) => Action::Select(SelectMotion::Last),
        (MoveToLast, Inspector) => Action::Scroll {
            target: ScrollTarget::Inspector,
            direction: ScrollDirection::Down,
            amount: ScrollAmount::ToEnd,
        },
        (MoveToLast, ResultScroll | ResultRowActive | ResultCellActive) => Action::Scroll {
            target: ScrollTarget::Result,
            direction: ScrollDirection::Down,
            amount: ScrollAmount::ToEnd,
        },

        // ViewportTop
        (ViewportTop, Explorer) => Action::Select(SelectMotion::ViewportTop),
        (ViewportTop, ResultScroll | ResultRowActive | ResultCellActive) => Action::Scroll {
            target: ScrollTarget::Result,
            direction: ScrollDirection::Up,
            amount: ScrollAmount::ViewportTop,
        },

        // ViewportMiddle
        (ViewportMiddle, Explorer) => Action::Select(SelectMotion::ViewportMiddle),
        (ViewportMiddle, ResultScroll | ResultRowActive | ResultCellActive) => Action::Scroll {
            target: ScrollTarget::Result,
            direction: ScrollDirection::Up,
            amount: ScrollAmount::ViewportMiddle,
        },

        // ViewportBottom
        (ViewportBottom, Explorer) => Action::Select(SelectMotion::ViewportBottom),
        (ViewportBottom, ResultScroll | ResultRowActive | ResultCellActive) => Action::Scroll {
            target: ScrollTarget::Result,
            direction: ScrollDirection::Down,
            amount: ScrollAmount::ViewportBottom,
        },

        // MoveLeft
        (MoveLeft, Explorer) => Action::Scroll {
            target: ScrollTarget::Explorer,
            direction: ScrollDirection::Left,
            amount: ScrollAmount::Line,
        },
        (MoveLeft, Inspector) => Action::Scroll {
            target: ScrollTarget::Inspector,
            direction: ScrollDirection::Left,
            amount: ScrollAmount::Line,
        },
        (MoveLeft, ResultScroll | ResultRowActive) => Action::Scroll {
            target: ScrollTarget::Result,
            direction: ScrollDirection::Left,
            amount: ScrollAmount::Line,
        },
        (MoveLeft, ResultCellActive) => Action::ResultCellLeft,

        // MoveRight
        (MoveRight, Explorer) => Action::Scroll {
            target: ScrollTarget::Explorer,
            direction: ScrollDirection::Right,
            amount: ScrollAmount::Line,
        },
        (MoveRight, Inspector) => Action::Scroll {
            target: ScrollTarget::Inspector,
            direction: ScrollDirection::Right,
            amount: ScrollAmount::Line,
        },
        (MoveRight, ResultScroll | ResultRowActive) => Action::Scroll {
            target: ScrollTarget::Result,
            direction: ScrollDirection::Right,
            amount: ScrollAmount::Line,
        },
        (MoveRight, ResultCellActive) => Action::ResultCellRight,

        // HalfPageDown
        (HalfPageDown, Explorer) => Action::Select(SelectMotion::HalfPageDown),
        (HalfPageDown, Inspector) => Action::Scroll {
            target: ScrollTarget::Inspector,
            direction: ScrollDirection::Down,
            amount: ScrollAmount::HalfPage,
        },
        (HalfPageDown, ResultScroll | ResultRowActive | ResultCellActive) => Action::Scroll {
            target: ScrollTarget::Result,
            direction: ScrollDirection::Down,
            amount: ScrollAmount::HalfPage,
        },

        // HalfPageUp
        (HalfPageUp, Explorer) => Action::Select(SelectMotion::HalfPageUp),
        (HalfPageUp, Inspector) => Action::Scroll {
            target: ScrollTarget::Inspector,
            direction: ScrollDirection::Up,
            amount: ScrollAmount::HalfPage,
        },
        (HalfPageUp, ResultScroll | ResultRowActive | ResultCellActive) => Action::Scroll {
            target: ScrollTarget::Result,
            direction: ScrollDirection::Up,
            amount: ScrollAmount::HalfPage,
        },

        // FullPageDown
        (FullPageDown, Explorer) => Action::Select(SelectMotion::FullPageDown),
        (FullPageDown, Inspector) => Action::Scroll {
            target: ScrollTarget::Inspector,
            direction: ScrollDirection::Down,
            amount: ScrollAmount::FullPage,
        },
        (FullPageDown, ResultScroll | ResultRowActive | ResultCellActive) => Action::Scroll {
            target: ScrollTarget::Result,
            direction: ScrollDirection::Down,
            amount: ScrollAmount::FullPage,
        },

        // FullPageUp
        (FullPageUp, Explorer) => Action::Select(SelectMotion::FullPageUp),
        (FullPageUp, Inspector) => Action::Scroll {
            target: ScrollTarget::Inspector,
            direction: ScrollDirection::Up,
            amount: ScrollAmount::FullPage,
        },
        (FullPageUp, ResultScroll | ResultRowActive | ResultCellActive) => Action::Scroll {
            target: ScrollTarget::Result,
            direction: ScrollDirection::Up,
            amount: ScrollAmount::FullPage,
        },

        // ScrollCursorCenter
        (ScrollCursorCenter, Explorer) => Action::ScrollToCursor {
            target: ScrollToCursorTarget::Explorer,
            position: CursorPosition::Center,
        },
        (ScrollCursorCenter, ResultScroll | ResultRowActive | ResultCellActive) => {
            Action::ScrollToCursor {
                target: ScrollToCursorTarget::Result,
                position: CursorPosition::Center,
            }
        }

        // ScrollCursorTop
        (ScrollCursorTop, Explorer) => Action::ScrollToCursor {
            target: ScrollToCursorTarget::Explorer,
            position: CursorPosition::Top,
        },
        (ScrollCursorTop, ResultScroll | ResultRowActive | ResultCellActive) => {
            Action::ScrollToCursor {
                target: ScrollToCursorTarget::Result,
                position: CursorPosition::Top,
            }
        }

        // ScrollCursorBottom
        (ScrollCursorBottom, Explorer) => Action::ScrollToCursor {
            target: ScrollToCursorTarget::Explorer,
            position: CursorPosition::Bottom,
        },
        (ScrollCursorBottom, ResultScroll | ResultRowActive | ResultCellActive) => {
            Action::ScrollToCursor {
                target: ScrollToCursorTarget::Result,
                position: CursorPosition::Bottom,
            }
        }

        (
            ViewportTop | ViewportMiddle | ViewportBottom | ScrollCursorCenter | ScrollCursorTop
            | ScrollCursorBottom,
            Inspector,
        ) => Action::None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::app::model::app_state::AppState;
    use crate::app::model::shared::focused_pane::FocusedPane;
    use crate::app::model::shared::ui_state::FocusMode;
    use crate::app::update::input::keybindings::{Key, KeyCombo};
    use rstest::rstest;

    // =========================================================================
    // NavigationContext::from_state
    // =========================================================================

    fn make_state(
        focused_pane: FocusedPane,
        focus_mode: bool,
        row: Option<usize>,
        cell: Option<usize>,
    ) -> AppState {
        let mut state = AppState::new("test".to_string());
        state.ui.focused_pane = focused_pane;
        state.ui.focus_mode = if focus_mode {
            FocusMode::focused(focused_pane)
        } else {
            FocusMode::Normal
        };
        if let Some(r) = row {
            state.result_interaction.enter_row(r);
            if let Some(c) = cell {
                state.result_interaction.enter_cell(c);
            }
        }
        state
    }

    #[rstest]
    #[case(FocusedPane::Explorer, false, None, None, NavigationContext::Explorer)]
    #[case(
        FocusedPane::Inspector,
        false,
        None,
        None,
        NavigationContext::Inspector
    )]
    #[case(
        FocusedPane::Result,
        false,
        None,
        None,
        NavigationContext::ResultScroll
    )]
    #[case(
        FocusedPane::Result,
        false,
        Some(0),
        None,
        NavigationContext::ResultRowActive
    )]
    #[case(
        FocusedPane::Result,
        false,
        Some(0),
        Some(0),
        NavigationContext::ResultCellActive
    )]
    #[case(
        FocusedPane::Explorer,
        true,
        None,
        None,
        NavigationContext::ResultScroll
    )]
    #[case(
        FocusedPane::Inspector,
        true,
        None,
        None,
        NavigationContext::ResultScroll
    )]
    fn from_state_derives_correct_context(
        #[case] pane: FocusedPane,
        #[case] focus_mode: bool,
        #[case] row: Option<usize>,
        #[case] cell: Option<usize>,
        #[case] expected: NavigationContext,
    ) {
        let state = make_state(pane, focus_mode, row, cell);
        assert_eq!(NavigationContext::from_state(&state), expected);
    }

    // =========================================================================
    // map_nav_intent — positive cases
    // =========================================================================

    #[rstest]
    #[case(KeyCombo::plain(Key::Char('j')), NavIntent::MoveDown)]
    #[case(KeyCombo::plain(Key::Down), NavIntent::MoveDown)]
    #[case(KeyCombo::ctrl(Key::Char('n')), NavIntent::MoveDown)]
    #[case(KeyCombo::plain(Key::Char('k')), NavIntent::MoveUp)]
    #[case(KeyCombo::plain(Key::Up), NavIntent::MoveUp)]
    #[case(KeyCombo::ctrl(Key::Char('p')), NavIntent::MoveUp)]
    #[case(KeyCombo::plain(Key::Char('g')), NavIntent::MoveToFirst)]
    #[case(KeyCombo::plain(Key::Home), NavIntent::MoveToFirst)]
    #[case(KeyCombo::plain(Key::Char('G')), NavIntent::MoveToLast)]
    #[case(KeyCombo::plain(Key::End), NavIntent::MoveToLast)]
    #[case(KeyCombo::plain(Key::Char('H')), NavIntent::ViewportTop)]
    #[case(KeyCombo::plain(Key::Char('M')), NavIntent::ViewportMiddle)]
    #[case(KeyCombo::plain(Key::Char('L')), NavIntent::ViewportBottom)]
    #[case(KeyCombo::plain(Key::Char('h')), NavIntent::MoveLeft)]
    #[case(KeyCombo::plain(Key::Left), NavIntent::MoveLeft)]
    #[case(KeyCombo::plain(Key::Char('l')), NavIntent::MoveRight)]
    #[case(KeyCombo::plain(Key::Right), NavIntent::MoveRight)]
    #[case(KeyCombo::plain(Key::PageDown), NavIntent::FullPageDown)]
    #[case(KeyCombo::plain(Key::PageUp), NavIntent::FullPageUp)]
    #[case(KeyCombo::ctrl(Key::Char('d')), NavIntent::HalfPageDown)]
    #[case(KeyCombo::ctrl(Key::Char('u')), NavIntent::HalfPageUp)]
    #[case(KeyCombo::ctrl(Key::Char('f')), NavIntent::FullPageDown)]
    #[case(KeyCombo::ctrl(Key::Char('b')), NavIntent::FullPageUp)]
    fn map_nav_intent_positive(#[case] combo: KeyCombo, #[case] expected: NavIntent) {
        assert_eq!(map_nav_intent(&combo), Some(expected));
    }

    // =========================================================================
    // map_nav_intent — negative cases
    // =========================================================================

    #[rstest]
    #[case(KeyCombo::plain(Key::Char('q')))]
    #[case(KeyCombo::plain(Key::Char('?')))]
    #[case(KeyCombo::plain(Key::Char(':')))]
    #[case(KeyCombo::plain(Key::Char('s')))]
    #[case(KeyCombo::plain(Key::Char('y')))]
    #[case(KeyCombo::plain(Key::Char('d')))]
    #[case(KeyCombo::plain(Key::Esc))]
    #[case(KeyCombo::plain(Key::Enter))]
    #[case(KeyCombo::plain(Key::Tab))]
    #[case(KeyCombo::ctrl(Key::Char('h')))]
    #[case(KeyCombo::ctrl(Key::Char('k')))]
    #[case(KeyCombo::ctrl(Key::Char('r')))]
    #[case(KeyCombo::ctrl(Key::Char('o')))]
    #[case(KeyCombo::ctrl(Key::Char('e')))]
    #[case(KeyCombo::alt(Key::Char('j')))]
    #[case(KeyCombo::shift(Key::Char('j')))]
    // Shift guard contract: shift+uppercase is blocked at NavIntent level.
    // The key_translator normalizes these before they reach here, but
    // the guard documents the contract for any caller bypassing the translator.
    #[case(KeyCombo::shift(Key::Char('G')))]
    #[case(KeyCombo::shift(Key::Char('H')))]
    #[case(KeyCombo::shift(Key::Char('M')))]
    #[case(KeyCombo::shift(Key::Char('L')))]
    fn map_nav_intent_negative(#[case] combo: KeyCombo) {
        assert_eq!(map_nav_intent(&combo), None);
    }

    // =========================================================================
    // resolve — full 80-pattern matrix
    // =========================================================================

    use NavIntent::*;
    use NavigationContext::*;

    #[rstest]
    // MoveDown (5)
    #[case(MoveDown, Explorer, Action::Select(SelectMotion::Next))]
    #[case(MoveDown, Inspector, Action::Scroll { target: ScrollTarget::Inspector, direction: ScrollDirection::Down, amount: ScrollAmount::Line })]
    #[case(MoveDown, ResultScroll, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Down, amount: ScrollAmount::Line })]
    #[case(MoveDown, ResultRowActive, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Down, amount: ScrollAmount::Line })]
    #[case(MoveDown, ResultCellActive, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Down, amount: ScrollAmount::Line })]
    // MoveUp (5)
    #[case(MoveUp, Explorer, Action::Select(SelectMotion::Previous))]
    #[case(MoveUp, Inspector, Action::Scroll { target: ScrollTarget::Inspector, direction: ScrollDirection::Up, amount: ScrollAmount::Line })]
    #[case(MoveUp, ResultScroll, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Up, amount: ScrollAmount::Line })]
    #[case(MoveUp, ResultRowActive, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Up, amount: ScrollAmount::Line })]
    #[case(MoveUp, ResultCellActive, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Up, amount: ScrollAmount::Line })]
    // MoveToFirst (5)
    #[case(MoveToFirst, Explorer, Action::Select(SelectMotion::First))]
    #[case(MoveToFirst, Inspector, Action::Scroll { target: ScrollTarget::Inspector, direction: ScrollDirection::Up, amount: ScrollAmount::ToStart })]
    #[case(MoveToFirst, ResultScroll, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Up, amount: ScrollAmount::ToStart })]
    #[case(MoveToFirst, ResultRowActive, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Up, amount: ScrollAmount::ToStart })]
    #[case(MoveToFirst, ResultCellActive, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Up, amount: ScrollAmount::ToStart })]
    // MoveToLast (5)
    #[case(MoveToLast, Explorer, Action::Select(SelectMotion::Last))]
    #[case(MoveToLast, Inspector, Action::Scroll { target: ScrollTarget::Inspector, direction: ScrollDirection::Down, amount: ScrollAmount::ToEnd })]
    #[case(MoveToLast, ResultScroll, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Down, amount: ScrollAmount::ToEnd })]
    #[case(MoveToLast, ResultRowActive, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Down, amount: ScrollAmount::ToEnd })]
    #[case(MoveToLast, ResultCellActive, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Down, amount: ScrollAmount::ToEnd })]
    // ViewportTop (5)
    #[case(ViewportTop, Explorer, Action::Select(SelectMotion::ViewportTop))]
    #[case(ViewportTop, Inspector, Action::None)]
    #[case(ViewportTop, ResultScroll, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Up, amount: ScrollAmount::ViewportTop })]
    #[case(ViewportTop, ResultRowActive, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Up, amount: ScrollAmount::ViewportTop })]
    #[case(ViewportTop, ResultCellActive, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Up, amount: ScrollAmount::ViewportTop })]
    // ViewportMiddle (5)
    #[case(ViewportMiddle, Explorer, Action::Select(SelectMotion::ViewportMiddle))]
    #[case(ViewportMiddle, Inspector, Action::None)]
    #[case(ViewportMiddle, ResultScroll, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Up, amount: ScrollAmount::ViewportMiddle })]
    #[case(ViewportMiddle, ResultRowActive, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Up, amount: ScrollAmount::ViewportMiddle })]
    #[case(ViewportMiddle, ResultCellActive, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Up, amount: ScrollAmount::ViewportMiddle })]
    // ViewportBottom (5)
    #[case(ViewportBottom, Explorer, Action::Select(SelectMotion::ViewportBottom))]
    #[case(ViewportBottom, Inspector, Action::None)]
    #[case(ViewportBottom, ResultScroll, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Down, amount: ScrollAmount::ViewportBottom })]
    #[case(ViewportBottom, ResultRowActive, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Down, amount: ScrollAmount::ViewportBottom })]
    #[case(ViewportBottom, ResultCellActive, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Down, amount: ScrollAmount::ViewportBottom })]
    // MoveLeft (5)
    #[case(MoveLeft, Explorer, Action::Scroll { target: ScrollTarget::Explorer, direction: ScrollDirection::Left, amount: ScrollAmount::Line })]
    #[case(MoveLeft, Inspector, Action::Scroll { target: ScrollTarget::Inspector, direction: ScrollDirection::Left, amount: ScrollAmount::Line })]
    #[case(MoveLeft, ResultScroll, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Left, amount: ScrollAmount::Line })]
    #[case(MoveLeft, ResultRowActive, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Left, amount: ScrollAmount::Line })]
    #[case(MoveLeft, ResultCellActive, Action::ResultCellLeft)]
    // MoveRight (5)
    #[case(MoveRight, Explorer, Action::Scroll { target: ScrollTarget::Explorer, direction: ScrollDirection::Right, amount: ScrollAmount::Line })]
    #[case(MoveRight, Inspector, Action::Scroll { target: ScrollTarget::Inspector, direction: ScrollDirection::Right, amount: ScrollAmount::Line })]
    #[case(MoveRight, ResultScroll, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Right, amount: ScrollAmount::Line })]
    #[case(MoveRight, ResultRowActive, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Right, amount: ScrollAmount::Line })]
    #[case(MoveRight, ResultCellActive, Action::ResultCellRight)]
    // HalfPageDown (5)
    #[case(HalfPageDown, Explorer, Action::Select(SelectMotion::HalfPageDown))]
    #[case(HalfPageDown, Inspector, Action::Scroll { target: ScrollTarget::Inspector, direction: ScrollDirection::Down, amount: ScrollAmount::HalfPage })]
    #[case(HalfPageDown, ResultScroll, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Down, amount: ScrollAmount::HalfPage })]
    #[case(HalfPageDown, ResultRowActive, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Down, amount: ScrollAmount::HalfPage })]
    #[case(HalfPageDown, ResultCellActive, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Down, amount: ScrollAmount::HalfPage })]
    // HalfPageUp (5)
    #[case(HalfPageUp, Explorer, Action::Select(SelectMotion::HalfPageUp))]
    #[case(HalfPageUp, Inspector, Action::Scroll { target: ScrollTarget::Inspector, direction: ScrollDirection::Up, amount: ScrollAmount::HalfPage })]
    #[case(HalfPageUp, ResultScroll, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Up, amount: ScrollAmount::HalfPage })]
    #[case(HalfPageUp, ResultRowActive, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Up, amount: ScrollAmount::HalfPage })]
    #[case(HalfPageUp, ResultCellActive, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Up, amount: ScrollAmount::HalfPage })]
    // FullPageDown (5)
    #[case(FullPageDown, Explorer, Action::Select(SelectMotion::FullPageDown))]
    #[case(FullPageDown, Inspector, Action::Scroll { target: ScrollTarget::Inspector, direction: ScrollDirection::Down, amount: ScrollAmount::FullPage })]
    #[case(FullPageDown, ResultScroll, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Down, amount: ScrollAmount::FullPage })]
    #[case(FullPageDown, ResultRowActive, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Down, amount: ScrollAmount::FullPage })]
    #[case(FullPageDown, ResultCellActive, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Down, amount: ScrollAmount::FullPage })]
    // FullPageUp (5)
    #[case(FullPageUp, Explorer, Action::Select(SelectMotion::FullPageUp))]
    #[case(FullPageUp, Inspector, Action::Scroll { target: ScrollTarget::Inspector, direction: ScrollDirection::Up, amount: ScrollAmount::FullPage })]
    #[case(FullPageUp, ResultScroll, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Up, amount: ScrollAmount::FullPage })]
    #[case(FullPageUp, ResultRowActive, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Up, amount: ScrollAmount::FullPage })]
    #[case(FullPageUp, ResultCellActive, Action::Scroll { target: ScrollTarget::Result, direction: ScrollDirection::Up, amount: ScrollAmount::FullPage })]
    // ScrollCursorCenter (5)
    #[case(ScrollCursorCenter, Explorer, Action::ScrollToCursor { target: ScrollToCursorTarget::Explorer, position: CursorPosition::Center })]
    #[case(ScrollCursorCenter, Inspector, Action::None)]
    #[case(ScrollCursorCenter, ResultScroll, Action::ScrollToCursor { target: ScrollToCursorTarget::Result, position: CursorPosition::Center })]
    #[case(ScrollCursorCenter, ResultRowActive, Action::ScrollToCursor { target: ScrollToCursorTarget::Result, position: CursorPosition::Center })]
    #[case(ScrollCursorCenter, ResultCellActive, Action::ScrollToCursor { target: ScrollToCursorTarget::Result, position: CursorPosition::Center })]
    // ScrollCursorTop (5)
    #[case(ScrollCursorTop, Explorer, Action::ScrollToCursor { target: ScrollToCursorTarget::Explorer, position: CursorPosition::Top })]
    #[case(ScrollCursorTop, Inspector, Action::None)]
    #[case(ScrollCursorTop, ResultScroll, Action::ScrollToCursor { target: ScrollToCursorTarget::Result, position: CursorPosition::Top })]
    #[case(ScrollCursorTop, ResultRowActive, Action::ScrollToCursor { target: ScrollToCursorTarget::Result, position: CursorPosition::Top })]
    #[case(ScrollCursorTop, ResultCellActive, Action::ScrollToCursor { target: ScrollToCursorTarget::Result, position: CursorPosition::Top })]
    // ScrollCursorBottom (5)
    #[case(ScrollCursorBottom, Explorer, Action::ScrollToCursor { target: ScrollToCursorTarget::Explorer, position: CursorPosition::Bottom })]
    #[case(ScrollCursorBottom, Inspector, Action::None)]
    #[case(ScrollCursorBottom, ResultScroll, Action::ScrollToCursor { target: ScrollToCursorTarget::Result, position: CursorPosition::Bottom })]
    #[case(ScrollCursorBottom, ResultRowActive, Action::ScrollToCursor { target: ScrollToCursorTarget::Result, position: CursorPosition::Bottom })]
    #[case(ScrollCursorBottom, ResultCellActive, Action::ScrollToCursor { target: ScrollToCursorTarget::Result, position: CursorPosition::Bottom })]
    fn resolve_matrix(
        #[case] intent: NavIntent,
        #[case] ctx: NavigationContext,
        #[case] expected: Action,
    ) {
        let actual = resolve(intent, ctx);
        assert_eq!(
            format!("{actual:?}"),
            format!("{expected:?}"),
            "resolve({intent:?}, {ctx:?})"
        );
    }
}