Skip to main content

void_graph/
event.rs

1//! Event handling for the TUI application.
2//!
3//! This module is adapted from [Serie](https://github.com/lusingander/serie),
4//! a git commit graph visualizer. Original code by lusingander, licensed under MIT.
5
6use std::{
7    fmt::{self, Debug, Formatter},
8    sync::mpsc,
9    thread,
10};
11
12use ratatui::crossterm::event::KeyEvent;
13use serde::{
14    de::{self, Deserializer, Visitor},
15    Deserialize,
16};
17
18pub enum AppEvent {
19    Key(KeyEvent),
20    Resize(usize, usize),
21    Quit,
22    OpenDetail,
23    CloseDetail,
24    ClearDetail,
25    OpenUserCommand(usize),
26    CloseUserCommand,
27    ClearUserCommand,
28    OpenRefs,
29    CloseRefs,
30    OpenHelp,
31    CloseHelp,
32    ClearHelp,
33    SelectNewerCommit,
34    SelectOlderCommit,
35    SelectParentCommit,
36    CopyToClipboard { name: String, value: String },
37    ClearStatusLine,
38    UpdateStatusInput(String, Option<u16>, Option<String>),
39    NotifyInfo(String),
40    NotifySuccess(String),
41    NotifyWarn(String),
42    NotifyError(String),
43}
44
45#[derive(Clone)]
46pub struct Sender {
47    tx: mpsc::Sender<AppEvent>,
48}
49
50impl Sender {
51    pub fn send(&self, event: AppEvent) {
52        self.tx.send(event).unwrap();
53    }
54}
55
56impl Debug for Sender {
57    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
58        write!(f, "Sender")
59    }
60}
61
62pub struct Receiver {
63    rx: mpsc::Receiver<AppEvent>,
64}
65
66impl Receiver {
67    pub fn recv(&self) -> AppEvent {
68        self.rx.recv().unwrap()
69    }
70}
71
72pub fn init() -> (Sender, Receiver) {
73    let (tx, rx) = mpsc::channel();
74    let tx = Sender { tx };
75    let rx = Receiver { rx };
76
77    let event_tx = tx.clone();
78    thread::spawn(move || loop {
79        match ratatui::crossterm::event::read() {
80            Ok(e) => match e {
81                ratatui::crossterm::event::Event::Key(key) => {
82                    event_tx.send(AppEvent::Key(key));
83                }
84                ratatui::crossterm::event::Event::Resize(w, h) => {
85                    event_tx.send(AppEvent::Resize(w as usize, h as usize));
86                }
87                _ => {}
88            },
89            Err(_) => {
90                // Terminal gone or unrecoverable — request clean shutdown
91                event_tx.send(AppEvent::Quit);
92                break;
93            }
94        }
95    });
96
97    (tx, rx)
98}
99
100// The event triggered by user's key input
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
102pub enum UserEvent {
103    ForceQuit,
104    Quit,
105    HelpToggle,
106    Cancel,
107    Close,
108    NavigateUp,
109    NavigateDown,
110    NavigateRight,
111    NavigateLeft,
112    SelectUp,
113    SelectDown,
114    GoToTop,
115    GoToBottom,
116    GoToParent,
117    ScrollUp,
118    ScrollDown,
119    PageUp,
120    PageDown,
121    HalfPageUp,
122    HalfPageDown,
123    SelectTop,
124    SelectMiddle,
125    SelectBottom,
126    GoToNext,
127    GoToPrevious,
128    Confirm,
129    RefListToggle,
130    Search,
131    UserCommandViewToggle(usize),
132    IgnoreCaseToggle,
133    FuzzyToggle,
134    ShortCopy,
135    FullCopy,
136    Unknown,
137}
138
139impl<'de> Deserialize<'de> for UserEvent {
140    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
141    where
142        D: Deserializer<'de>,
143    {
144        struct UserEventVisitor;
145
146        impl<'de> Visitor<'de> for UserEventVisitor {
147            type Value = UserEvent;
148
149            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
150                formatter.write_str("a string representing a user event")
151            }
152
153            fn visit_str<E>(self, value: &str) -> Result<UserEvent, E>
154            where
155                E: de::Error,
156            {
157                if let Some(num_str) = value.strip_prefix("user_command_view_toggle_") {
158                    if let Ok(num) = num_str.parse::<usize>() {
159                        Ok(UserEvent::UserCommandViewToggle(num))
160                    } else {
161                        let msg = format!("Invalid user_command_view_toggle_n format: {}", value);
162                        Err(de::Error::custom(msg))
163                    }
164                } else {
165                    match value {
166                        "force_quit" => Ok(UserEvent::ForceQuit),
167                        "quit" => Ok(UserEvent::Quit),
168                        "help_toggle" => Ok(UserEvent::HelpToggle),
169                        "cancel" => Ok(UserEvent::Cancel),
170                        "close" => Ok(UserEvent::Close),
171                        "navigate_up" => Ok(UserEvent::NavigateUp),
172                        "navigate_down" => Ok(UserEvent::NavigateDown),
173                        "navigate_right" => Ok(UserEvent::NavigateRight),
174                        "navigate_left" => Ok(UserEvent::NavigateLeft),
175                        "select_up" => Ok(UserEvent::SelectUp),
176                        "select_down" => Ok(UserEvent::SelectDown),
177                        "go_to_top" => Ok(UserEvent::GoToTop),
178                        "go_to_bottom" => Ok(UserEvent::GoToBottom),
179                        "go_to_parent" => Ok(UserEvent::GoToParent),
180                        "scroll_up" => Ok(UserEvent::ScrollUp),
181                        "scroll_down" => Ok(UserEvent::ScrollDown),
182                        "page_up" => Ok(UserEvent::PageUp),
183                        "page_down" => Ok(UserEvent::PageDown),
184                        "half_page_up" => Ok(UserEvent::HalfPageUp),
185                        "half_page_down" => Ok(UserEvent::HalfPageDown),
186                        "select_top" => Ok(UserEvent::SelectTop),
187                        "select_middle" => Ok(UserEvent::SelectMiddle),
188                        "select_bottom" => Ok(UserEvent::SelectBottom),
189                        "go_to_next" => Ok(UserEvent::GoToNext),
190                        "go_to_previous" => Ok(UserEvent::GoToPrevious),
191                        "confirm" => Ok(UserEvent::Confirm),
192                        "ref_list_toggle" => Ok(UserEvent::RefListToggle),
193                        "search" => Ok(UserEvent::Search),
194                        "ignore_case_toggle" => Ok(UserEvent::IgnoreCaseToggle),
195                        "fuzzy_toggle" => Ok(UserEvent::FuzzyToggle),
196                        "short_copy" => Ok(UserEvent::ShortCopy),
197                        "full_copy" => Ok(UserEvent::FullCopy),
198                        _ => {
199                            let msg = format!("Unknown user event: {}", value);
200                            Err(de::Error::custom(msg))
201                        }
202                    }
203                }
204            }
205        }
206
207        deserializer.deserialize_str(UserEventVisitor)
208    }
209}
210
211impl UserEvent {
212    pub fn is_countable(&self) -> bool {
213        matches!(
214            self,
215            UserEvent::NavigateUp
216                | UserEvent::NavigateDown
217                | UserEvent::ScrollUp
218                | UserEvent::ScrollDown
219                | UserEvent::GoToParent
220                | UserEvent::PageUp
221                | UserEvent::PageDown
222                | UserEvent::HalfPageUp
223                | UserEvent::HalfPageDown
224        )
225    }
226}
227
228#[derive(Debug, Clone, Copy, PartialEq, Eq)]
229pub struct UserEventWithCount {
230    pub event: UserEvent,
231    pub count: usize,
232}
233
234impl UserEventWithCount {
235    pub fn new(event: UserEvent, count: usize) -> Self {
236        Self {
237            event,
238            count: if count == 0 { 1 } else { count },
239        }
240    }
241
242    pub fn from_event(event: UserEvent) -> Self {
243        Self::new(event, 1)
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[test]
252    fn test_user_event_with_count_new() {
253        let event = UserEventWithCount::new(UserEvent::NavigateUp, 5);
254        assert_eq!(event.event, UserEvent::NavigateUp);
255        assert_eq!(event.count, 5);
256    }
257
258    #[test]
259    fn test_user_event_with_count_new_zero_count() {
260        let event = UserEventWithCount::new(UserEvent::NavigateDown, 0);
261        assert_eq!(event.event, UserEvent::NavigateDown);
262        assert_eq!(event.count, 1); // zero should be converted to 1
263    }
264
265    #[test]
266    fn test_user_event_with_count_from_event() {
267        let event = UserEventWithCount::from_event(UserEvent::NavigateLeft);
268        assert_eq!(event.event, UserEvent::NavigateLeft);
269        assert_eq!(event.count, 1);
270    }
271
272    #[test]
273    fn test_user_event_with_count_equality() {
274        let event1 = UserEventWithCount::new(UserEvent::ScrollUp, 3);
275        let event2 = UserEventWithCount::new(UserEvent::ScrollUp, 3);
276        let event3 = UserEventWithCount::new(UserEvent::ScrollDown, 3);
277
278        assert_eq!(event1, event2);
279        assert_ne!(event1, event3);
280    }
281}