Skip to main content

binocular/preview/structured_log/
actions.rs

1use crate::preview::types::LogPreview;
2use crossterm::event::{KeyCode, KeyEvent};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub(crate) enum LogViewerOutcome {
6    None,
7    ExitApp,
8    FocusSearch,
9}
10
11pub(crate) enum LogViewerAction {
12    Filter(FilterAction),
13    Cursor(CursorAction),
14    Column(ColumnAction),
15    Modal(ModalAction),
16    TogglePause,
17    ToggleMark,
18    Copy { raw: bool },
19    ResetView,
20    Exit,
21}
22
23pub(crate) enum FilterAction {
24    StartEditing,
25    StopEditing,
26    Backspace,
27    Insert(char),
28}
29
30pub(crate) enum CursorAction {
31    ToNewest,
32    ToOldest,
33    Down(usize),
34    Up(usize),
35}
36
37pub(crate) enum ColumnAction {
38    MoveLeft,
39    MoveRight,
40    HideSelected,
41    IsolateSelected,
42    OpenPicker,
43    Resize(i32),
44}
45
46pub(crate) enum ModalAction {
47    Close,
48    Apply,
49    Down,
50    Up,
51    Toggle { advance: bool },
52}
53
54pub(crate) fn action_for_key(lp: &LogPreview, key: KeyEvent) -> Option<LogViewerAction> {
55    if lp.filter_state.col_modal.is_some() {
56        return modal_action_for_key(key).map(LogViewerAction::Modal);
57    }
58
59    if lp.filter_state.input_active {
60        return filter_action_for_key(key).map(LogViewerAction::Filter);
61    }
62
63    match key.code {
64        KeyCode::Char('/') => Some(LogViewerAction::Filter(FilterAction::StartEditing)),
65        KeyCode::Char('r') => Some(LogViewerAction::ResetView),
66        KeyCode::Char('G') => Some(LogViewerAction::Cursor(CursorAction::ToOldest)),
67        KeyCode::Char('g') => Some(LogViewerAction::Cursor(CursorAction::ToNewest)),
68        KeyCode::Char('j') | KeyCode::Down => Some(LogViewerAction::Cursor(CursorAction::Down(1))),
69        KeyCode::Char('k') | KeyCode::Up => Some(LogViewerAction::Cursor(CursorAction::Up(1))),
70        KeyCode::Char('d') => Some(LogViewerAction::Cursor(CursorAction::Down(20))),
71        KeyCode::Char('u') => Some(LogViewerAction::Cursor(CursorAction::Up(20))),
72        KeyCode::Char('h') | KeyCode::Left => Some(LogViewerAction::Column(ColumnAction::MoveLeft)),
73        KeyCode::Char('l') | KeyCode::Right => {
74            Some(LogViewerAction::Column(ColumnAction::MoveRight))
75        }
76        KeyCode::Char('H') => Some(LogViewerAction::Column(ColumnAction::HideSelected)),
77        KeyCode::Char('o') => Some(LogViewerAction::Column(ColumnAction::IsolateSelected)),
78        KeyCode::Char('a') => Some(LogViewerAction::Column(ColumnAction::OpenPicker)),
79        KeyCode::Char('<') => Some(LogViewerAction::Column(ColumnAction::Resize(-5))),
80        KeyCode::Char('>') => Some(LogViewerAction::Column(ColumnAction::Resize(5))),
81        KeyCode::Char('p') => Some(LogViewerAction::TogglePause),
82        KeyCode::Tab => Some(LogViewerAction::ToggleMark),
83        KeyCode::Char('y') => Some(LogViewerAction::Copy { raw: false }),
84        KeyCode::Char('Y') => Some(LogViewerAction::Copy { raw: true }),
85        KeyCode::Esc | KeyCode::Char('q') => Some(LogViewerAction::Exit),
86        _ => None,
87    }
88}
89
90fn filter_action_for_key(key: KeyEvent) -> Option<FilterAction> {
91    match key.code {
92        KeyCode::Esc | KeyCode::Enter => Some(FilterAction::StopEditing),
93        KeyCode::Backspace => Some(FilterAction::Backspace),
94        KeyCode::Char(c) => Some(FilterAction::Insert(c)),
95        _ => None,
96    }
97}
98
99fn modal_action_for_key(key: KeyEvent) -> Option<ModalAction> {
100    match key.code {
101        KeyCode::Esc => Some(ModalAction::Close),
102        KeyCode::Enter => Some(ModalAction::Apply),
103        KeyCode::Char('j') | KeyCode::Down => Some(ModalAction::Down),
104        KeyCode::Char('k') | KeyCode::Up => Some(ModalAction::Up),
105        KeyCode::Char(' ') => Some(ModalAction::Toggle { advance: false }),
106        KeyCode::Tab => Some(ModalAction::Toggle { advance: true }),
107        _ => None,
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use crate::preview::structured_log::{preview_content, LogEntry, LogFormat, StructuredLog};
115    use crate::preview::PreviewContent;
116    use crossterm::event::KeyModifiers;
117
118    fn entry(fields: &[(&str, &str)], raw: &str) -> LogEntry {
119        LogEntry {
120            fields: fields
121                .iter()
122                .map(|(key, value)| (key.to_string(), value.to_string()))
123                .collect(),
124            raw: raw.to_string(),
125        }
126    }
127
128    #[test]
129    fn normal_mode_keys_map_to_explicit_actions() {
130        let PreviewContent::StructuredLog(preview) = preview_content(StructuredLog {
131            entries: vec![entry(&[("level", "info")], "level=info")],
132            total_lines: 1,
133            all_fields: vec!["level".to_string()],
134            format: LogFormat::Logfmt,
135        }) else {
136            panic!("expected structured log preview");
137        };
138
139        let key = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE);
140        assert!(matches!(
141            action_for_key(&preview, key),
142            Some(LogViewerAction::Cursor(CursorAction::Down(20)))
143        ));
144
145        let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
146        assert!(matches!(
147            action_for_key(&preview, key),
148            Some(LogViewerAction::Column(ColumnAction::OpenPicker))
149        ));
150    }
151}