Skip to main content

wisp_core/
reduce.rs

1use crate::{AlertState, ClientFocus, DirectoryRecord, DomainSnapshot, DomainState};
2
3#[derive(Debug, Clone, PartialEq)]
4pub enum DomainEvent {
5    SnapshotLoaded(DomainSnapshot),
6    FocusChanged {
7        client_id: String,
8        focus: ClientFocus,
9    },
10    AlertChanged {
11        window_id: String,
12        alerts: AlertState,
13    },
14    OutputChanged {
15        pane_id: String,
16    },
17    DirectoriesUpdated(Vec<DirectoryRecord>),
18}
19
20pub fn reduce_domain_event(state: &mut DomainState, event: DomainEvent) {
21    match event {
22        DomainEvent::SnapshotLoaded(snapshot) => {
23            let mut previous = state.previous_session_by_client.clone();
24            for (client_id, next_focus) in &snapshot.clients {
25                if let Some(current_focus) = state.clients.get(client_id)
26                    && current_focus.session_id != next_focus.session_id
27                {
28                    previous.insert(client_id.clone(), current_focus.session_id.clone());
29                }
30            }
31
32            state.sessions = snapshot.sessions;
33            state.clients = snapshot.clients;
34            state.directories = snapshot.directories;
35            state.previous_session_by_client = previous;
36            state.recompute_aggregates();
37        }
38        DomainEvent::FocusChanged { client_id, focus } => {
39            if let Some(current_focus) = state.clients.get(&client_id)
40                && current_focus.session_id != focus.session_id
41            {
42                state
43                    .previous_session_by_client
44                    .insert(client_id.clone(), current_focus.session_id.clone());
45            }
46
47            let session_id = focus.session_id.clone();
48            let window_id = focus.window_id.clone();
49            state.clients.insert(client_id, focus);
50            if state.config.notifications.clear_on_focus {
51                state.clear_unseen_for_window(&session_id, &window_id);
52            } else {
53                state.recompute_session_aggregate(&session_id);
54            }
55        }
56        DomainEvent::AlertChanged { window_id, alerts } => {
57            if let Some(session_id) = state.session_id_for_window(&window_id) {
58                if let Some(window) = state
59                    .sessions
60                    .get_mut(&session_id)
61                    .and_then(|session| session.windows.get_mut(&window_id))
62                {
63                    window.alerts = AlertState {
64                        unseen_output: window.alerts.unseen_output,
65                        ..alerts
66                    };
67                }
68                state.recompute_session_aggregate(&session_id);
69            }
70        }
71        DomainEvent::OutputChanged { pane_id } => {
72            if !state.config.notifications.track_unseen_output {
73                return;
74            }
75
76            if let Some((session_id, window_id)) = state.session_window_for_pane(&pane_id) {
77                let focused_session = state.focused_session_for_window(&window_id);
78                if focused_session.as_deref() == Some(session_id.as_str()) {
79                    return;
80                }
81
82                if let Some(window) = state
83                    .sessions
84                    .get_mut(&session_id)
85                    .and_then(|session| session.windows.get_mut(&window_id))
86                {
87                    window.has_unseen = true;
88                    window.alerts.unseen_output = true;
89                }
90                state.recompute_session_aggregate(&session_id);
91            }
92        }
93        DomainEvent::DirectoriesUpdated(entries) => {
94            state.directories = entries;
95        }
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use std::collections::BTreeMap;
102
103    use crate::{
104        AlertState, AttentionBadge, ClientFocus, DomainConfig, DomainEvent, DomainState,
105        NotificationConfig, PaneRecord, SessionRecord, SessionSortKey, WindowRecord,
106        reduce_domain_event,
107    };
108
109    fn seeded_state() -> DomainState {
110        DomainState {
111            sessions: BTreeMap::from([(
112                "alpha".to_string(),
113                SessionRecord {
114                    id: "alpha".to_string(),
115                    tmux_id: None,
116                    name: "alpha".to_string(),
117                    attached: true,
118                    windows: BTreeMap::from([(
119                        "alpha:1".to_string(),
120                        WindowRecord {
121                            id: "alpha:1".to_string(),
122                            index: 1,
123                            name: "shell".to_string(),
124                            active: true,
125                            panes: BTreeMap::from([(
126                                "alpha:1.1".to_string(),
127                                PaneRecord {
128                                    id: "alpha:1.1".to_string(),
129                                    index: 1,
130                                    title: None,
131                                    current_path: None,
132                                    current_command: None,
133                                    is_active: true,
134                                },
135                            )]),
136                            alerts: AlertState::default(),
137                            has_unseen: false,
138                            current_path: None,
139                            active_command: None,
140                        },
141                    )]),
142                    aggregate_alerts: Default::default(),
143                    has_unseen: false,
144                    sort_key: SessionSortKey::default(),
145                },
146            )]),
147            clients: BTreeMap::from([(
148                "client-1".to_string(),
149                ClientFocus {
150                    session_id: "alpha".to_string(),
151                    window_id: "alpha:1".to_string(),
152                    pane_id: Some("alpha:1.1".to_string()),
153                },
154            )]),
155            previous_session_by_client: BTreeMap::new(),
156            directories: Vec::new(),
157            config: DomainConfig {
158                notifications: NotificationConfig {
159                    track_unseen_output: true,
160                    clear_on_focus: true,
161                    show_silence: true,
162                },
163            },
164        }
165    }
166
167    #[test]
168    fn tracks_previous_session_per_client() {
169        let mut state = seeded_state();
170        state.sessions.insert(
171            "beta".to_string(),
172            SessionRecord {
173                id: "beta".to_string(),
174                tmux_id: None,
175                name: "beta".to_string(),
176                attached: false,
177                windows: BTreeMap::from([(
178                    "beta:1".to_string(),
179                    WindowRecord {
180                        id: "beta:1".to_string(),
181                        index: 1,
182                        name: "editor".to_string(),
183                        active: true,
184                        panes: BTreeMap::new(),
185                        alerts: AlertState::default(),
186                        has_unseen: false,
187                        current_path: None,
188                        active_command: None,
189                    },
190                )]),
191                aggregate_alerts: Default::default(),
192                has_unseen: false,
193                sort_key: SessionSortKey::default(),
194            },
195        );
196
197        reduce_domain_event(
198            &mut state,
199            DomainEvent::FocusChanged {
200                client_id: "client-1".to_string(),
201                focus: ClientFocus {
202                    session_id: "beta".to_string(),
203                    window_id: "beta:1".to_string(),
204                    pane_id: None,
205                },
206            },
207        );
208
209        assert_eq!(
210            state.previous_session_by_client.get("client-1"),
211            Some(&"alpha".to_string())
212        );
213    }
214
215    #[test]
216    fn marks_unseen_output_on_non_focused_windows() {
217        let mut state = seeded_state();
218        state.sessions.insert(
219            "beta".to_string(),
220            SessionRecord {
221                id: "beta".to_string(),
222                tmux_id: None,
223                name: "beta".to_string(),
224                attached: false,
225                windows: BTreeMap::from([(
226                    "beta:1".to_string(),
227                    WindowRecord {
228                        id: "beta:1".to_string(),
229                        index: 1,
230                        name: "logs".to_string(),
231                        active: true,
232                        panes: BTreeMap::from([(
233                            "beta:1.1".to_string(),
234                            PaneRecord {
235                                id: "beta:1.1".to_string(),
236                                index: 1,
237                                title: None,
238                                current_path: None,
239                                current_command: None,
240                                is_active: true,
241                            },
242                        )]),
243                        alerts: AlertState::default(),
244                        has_unseen: false,
245                        current_path: None,
246                        active_command: None,
247                    },
248                )]),
249                aggregate_alerts: Default::default(),
250                has_unseen: false,
251                sort_key: SessionSortKey::default(),
252            },
253        );
254
255        reduce_domain_event(
256            &mut state,
257            DomainEvent::OutputChanged {
258                pane_id: "beta:1.1".to_string(),
259            },
260        );
261
262        assert!(state.sessions["beta"].has_unseen);
263        assert_eq!(
264            state.sessions["beta"].aggregate_alerts.highest_priority,
265            AttentionBadge::Unseen
266        );
267    }
268
269    #[test]
270    fn clears_unseen_on_focus_when_configured() {
271        let mut state = seeded_state();
272        state.sessions.insert(
273            "beta".to_string(),
274            SessionRecord {
275                id: "beta".to_string(),
276                tmux_id: None,
277                name: "beta".to_string(),
278                attached: false,
279                windows: BTreeMap::from([(
280                    "beta:1".to_string(),
281                    WindowRecord {
282                        id: "beta:1".to_string(),
283                        index: 1,
284                        name: "logs".to_string(),
285                        active: true,
286                        panes: BTreeMap::new(),
287                        alerts: AlertState {
288                            unseen_output: true,
289                            ..AlertState::default()
290                        },
291                        has_unseen: true,
292                        current_path: None,
293                        active_command: None,
294                    },
295                )]),
296                aggregate_alerts: Default::default(),
297                has_unseen: true,
298                sort_key: SessionSortKey::default(),
299            },
300        );
301        state.recompute_aggregates();
302
303        reduce_domain_event(
304            &mut state,
305            DomainEvent::FocusChanged {
306                client_id: "client-1".to_string(),
307                focus: ClientFocus {
308                    session_id: "beta".to_string(),
309                    window_id: "beta:1".to_string(),
310                    pane_id: None,
311                },
312            },
313        );
314
315        assert!(!state.sessions["beta"].has_unseen);
316    }
317}