1use crate::buffer::{AppMode, Buffer, BufferAPI};
8use std::collections::VecDeque;
9use std::time::Instant;
10use tracing::{debug, info, warn};
11
12#[derive(Debug, Clone, PartialEq)]
14pub enum AppState {
15 Command,
17 Results,
19 Search { search_type: SearchType },
21 Help,
23 Debug,
25 History,
27 JumpToRow,
29 ColumnStats,
31}
32
33#[derive(Debug, Clone, PartialEq)]
35pub enum SearchType {
36 Vim, Column, Data, Fuzzy, }
41
42pub struct ShadowStateManager {
44 state: AppState,
46
47 previous_state: Option<AppState>,
49
50 history: VecDeque<StateTransition>,
52
53 transition_count: usize,
55
56 discrepancies: Vec<String>,
58}
59
60#[derive(Debug, Clone)]
61struct StateTransition {
62 timestamp: Instant,
63 from: AppState,
64 to: AppState,
65 trigger: String,
66}
67
68impl ShadowStateManager {
69 pub fn new() -> Self {
70 info!(target: "shadow_state", "Shadow state manager initialized");
71
72 Self {
73 state: AppState::Command,
74 previous_state: None,
75 history: VecDeque::with_capacity(100),
76 transition_count: 0,
77 discrepancies: Vec::new(),
78 }
79 }
80
81 pub fn observe_mode_change(&mut self, mode: AppMode, trigger: &str) {
83 let new_state = self.mode_to_state(mode.clone());
84
85 if new_state == self.state {
87 debug!(target: "shadow_state",
88 "Redundant mode change to {:?} ignored", mode);
89 } else {
90 self.transition_count += 1;
91
92 info!(target: "shadow_state",
93 "[#{}] {} -> {} (trigger: {})",
94 self.transition_count,
95 self.state_display(&self.state),
96 self.state_display(&new_state),
97 trigger
98 );
99
100 let transition = StateTransition {
102 timestamp: Instant::now(),
103 from: self.state.clone(),
104 to: new_state.clone(),
105 trigger: trigger.to_string(),
106 };
107
108 self.history.push_back(transition);
109 if self.history.len() > 100 {
110 self.history.pop_front();
111 }
112
113 self.previous_state = Some(self.state.clone());
115 self.state = new_state;
116
117 self.log_expected_side_effects();
119 }
120 }
121
122 pub fn set_mode(&mut self, mode: AppMode, buffer: &mut Buffer, trigger: &str) {
128 let new_state = self.mode_to_state(mode.clone());
129
130 if new_state == self.state {
132 debug!(target: "shadow_state",
133 "Redundant mode change to {:?} ignored", mode);
134 } else {
135 self.transition_count += 1;
136
137 info!(target: "shadow_state",
138 "[#{}] {} -> {} (trigger: {})",
139 self.transition_count,
140 self.state_display(&self.state),
141 self.state_display(&new_state),
142 trigger
143 );
144
145 let transition = StateTransition {
147 timestamp: Instant::now(),
148 from: self.state.clone(),
149 to: new_state.clone(),
150 trigger: trigger.to_string(),
151 };
152
153 self.history.push_back(transition);
154 if self.history.len() > 100 {
155 self.history.pop_front();
156 }
157
158 self.previous_state = Some(self.state.clone());
160 self.state = new_state;
161
162 buffer.set_mode(mode);
164
165 self.log_expected_side_effects();
167 }
168 }
169
170 pub fn switch_to_results(&mut self, buffer: &mut Buffer) {
172 self.set_mode(AppMode::Results, buffer, "switch_to_results");
173 }
174
175 pub fn switch_to_command(&mut self, buffer: &mut Buffer) {
177 self.set_mode(AppMode::Command, buffer, "switch_to_command");
178 }
179
180 pub fn start_search(&mut self, search_type: SearchType, buffer: &mut Buffer, trigger: &str) {
182 let mode = match search_type {
183 SearchType::Column => AppMode::ColumnSearch,
184 SearchType::Data | SearchType::Vim => AppMode::Search,
185 SearchType::Fuzzy => AppMode::FuzzyFilter,
186 };
187
188 self.state = AppState::Search {
190 search_type: search_type.clone(),
191 };
192 self.transition_count += 1;
193
194 info!(target: "shadow_state",
195 "[#{}] Starting {:?} search (trigger: {})",
196 self.transition_count, search_type, trigger
197 );
198
199 buffer.set_mode(mode);
201 }
202
203 pub fn exit_to_results(&mut self, buffer: &mut Buffer) {
205 self.set_mode(AppMode::Results, buffer, "exit_to_results");
206 }
207
208 pub fn observe_search_start(&mut self, search_type: SearchType, trigger: &str) {
212 let new_state = AppState::Search {
213 search_type: search_type.clone(),
214 };
215
216 if !matches!(self.state, AppState::Search { .. }) {
217 self.transition_count += 1;
218
219 info!(target: "shadow_state",
220 "[#{}] {} -> {:?} search (trigger: {})",
221 self.transition_count,
222 self.state_display(&self.state),
223 search_type,
224 trigger
225 );
226
227 self.previous_state = Some(self.state.clone());
228 self.state = new_state;
229
230 warn!(target: "shadow_state",
232 "⚠️ Search started - verify other search states were cleared!");
233 }
234 }
235
236 pub fn observe_search_end(&mut self, trigger: &str) {
238 if matches!(self.state, AppState::Search { .. }) {
239 let new_state = AppState::Results;
241
242 info!(target: "shadow_state",
243 "[#{}] Exiting search -> {} (trigger: {})",
244 self.transition_count,
245 self.state_display(&new_state),
246 trigger
247 );
248
249 self.previous_state = Some(self.state.clone());
250 self.state = new_state;
251
252 info!(target: "shadow_state",
254 "✓ Expected side effects: Clear search UI, restore navigation keys");
255 }
256 }
257
258 #[must_use]
260 pub fn is_search_active(&self) -> bool {
261 matches!(self.state, AppState::Search { .. })
262 }
263
264 #[must_use]
266 pub fn get_search_type(&self) -> Option<SearchType> {
267 if let AppState::Search { ref search_type } = self.state {
268 Some(search_type.clone())
269 } else {
270 None
271 }
272 }
273
274 #[must_use]
276 pub fn status_display(&self) -> String {
277 format!("[Shadow: {}]", self.state_display(&self.state))
278 }
279
280 #[must_use]
282 pub fn debug_info(&self) -> String {
283 let mut info = format!(
284 "Shadow State Debug (transitions: {})\n",
285 self.transition_count
286 );
287 info.push_str(&format!("Current: {:?}\n", self.state));
288
289 if !self.history.is_empty() {
290 info.push_str("\nRecent transitions:\n");
291 for transition in self.history.iter().rev().take(5) {
292 info.push_str(&format!(
293 " {:?} ago: {} -> {} ({})\n",
294 transition.timestamp.elapsed(),
295 self.state_display(&transition.from),
296 self.state_display(&transition.to),
297 transition.trigger
298 ));
299 }
300 }
301
302 if !self.discrepancies.is_empty() {
303 info.push_str("\n⚠️ Discrepancies detected:\n");
304 for disc in self.discrepancies.iter().rev().take(3) {
305 info.push_str(&format!(" - {disc}\n"));
306 }
307 }
308
309 info
310 }
311
312 pub fn report_discrepancy(&mut self, expected: &str, actual: &str) {
314 let msg = format!("Expected: {expected}, Actual: {actual}");
315 warn!(target: "shadow_state", "Discrepancy: {}", msg);
316 self.discrepancies.push(msg);
317 }
318
319 #[must_use]
325 pub fn get_state(&self) -> &AppState {
326 &self.state
327 }
328
329 #[must_use]
331 pub fn get_mode(&self) -> AppMode {
332 match &self.state {
333 AppState::Command => AppMode::Command,
334 AppState::Results => AppMode::Results,
335 AppState::Search { search_type } => match search_type {
336 SearchType::Column => AppMode::ColumnSearch,
337 SearchType::Data => AppMode::Search,
338 SearchType::Fuzzy => AppMode::FuzzyFilter,
339 SearchType::Vim => AppMode::Search, },
341 AppState::Help => AppMode::Help,
342 AppState::Debug => AppMode::Debug,
343 AppState::History => AppMode::History,
344 AppState::JumpToRow => AppMode::JumpToRow,
345 AppState::ColumnStats => AppMode::ColumnStats,
346 }
347 }
348
349 #[must_use]
351 pub fn is_in_results_mode(&self) -> bool {
352 matches!(self.state, AppState::Results)
353 }
354
355 #[must_use]
357 pub fn is_in_command_mode(&self) -> bool {
358 matches!(self.state, AppState::Command)
359 }
360
361 #[must_use]
363 pub fn is_in_search_mode(&self) -> bool {
364 matches!(self.state, AppState::Search { .. })
365 }
366
367 #[must_use]
369 pub fn is_in_help_mode(&self) -> bool {
370 matches!(self.state, AppState::Help)
371 }
372
373 #[must_use]
375 pub fn is_in_debug_mode(&self) -> bool {
376 matches!(self.state, AppState::Debug)
377 }
378
379 #[must_use]
381 pub fn is_in_history_mode(&self) -> bool {
382 matches!(self.state, AppState::History)
383 }
384
385 #[must_use]
387 pub fn is_in_jump_mode(&self) -> bool {
388 matches!(self.state, AppState::JumpToRow)
389 }
390
391 #[must_use]
393 pub fn is_in_column_stats_mode(&self) -> bool {
394 matches!(self.state, AppState::ColumnStats)
395 }
396
397 #[must_use]
399 pub fn is_in_column_search(&self) -> bool {
400 matches!(
401 self.state,
402 AppState::Search {
403 search_type: SearchType::Column
404 }
405 )
406 }
407
408 #[must_use]
410 pub fn is_in_data_search(&self) -> bool {
411 matches!(
412 self.state,
413 AppState::Search {
414 search_type: SearchType::Data
415 }
416 )
417 }
418
419 #[must_use]
421 pub fn is_in_fuzzy_filter(&self) -> bool {
422 matches!(
423 self.state,
424 AppState::Search {
425 search_type: SearchType::Fuzzy
426 }
427 )
428 }
429
430 #[must_use]
432 pub fn is_in_vim_search(&self) -> bool {
433 matches!(
434 self.state,
435 AppState::Search {
436 search_type: SearchType::Vim
437 }
438 )
439 }
440
441 #[must_use]
443 pub fn get_previous_state(&self) -> Option<&AppState> {
444 self.previous_state.as_ref()
445 }
446
447 #[must_use]
449 pub fn can_navigate(&self) -> bool {
450 self.is_in_results_mode()
451 }
452
453 #[must_use]
455 pub fn can_edit(&self) -> bool {
456 self.is_in_command_mode() || self.is_in_search_mode()
457 }
458
459 #[must_use]
461 pub fn get_transition_count(&self) -> usize {
462 self.transition_count
463 }
464
465 #[must_use]
467 pub fn get_last_transition(&self) -> Option<&StateTransition> {
468 self.history.back()
469 }
470
471 fn mode_to_state(&self, mode: AppMode) -> AppState {
474 match mode {
475 AppMode::Command => AppState::Command,
476 AppMode::Results => AppState::Results,
477 AppMode::Search | AppMode::ColumnSearch => {
478 if let AppState::Search { ref search_type } = self.state {
480 AppState::Search {
481 search_type: search_type.clone(),
482 }
483 } else {
484 let search_type = match mode {
486 AppMode::ColumnSearch => SearchType::Column,
487 _ => SearchType::Data,
488 };
489 AppState::Search { search_type }
490 }
491 }
492 AppMode::Help => AppState::Help,
493 AppMode::Debug | AppMode::PrettyQuery => AppState::Debug,
494 AppMode::History => AppState::History,
495 AppMode::JumpToRow => AppState::JumpToRow,
496 AppMode::ColumnStats => AppState::ColumnStats,
497 _ => self.state.clone(), }
499 }
500
501 fn state_display(&self, state: &AppState) -> String {
502 match state {
503 AppState::Command => "COMMAND".to_string(),
504 AppState::Results => "RESULTS".to_string(),
505 AppState::Search { search_type } => format!("SEARCH({search_type:?})"),
506 AppState::Help => "HELP".to_string(),
507 AppState::Debug => "DEBUG".to_string(),
508 AppState::History => "HISTORY".to_string(),
509 AppState::JumpToRow => "JUMP_TO_ROW".to_string(),
510 AppState::ColumnStats => "COLUMN_STATS".to_string(),
511 }
512 }
513
514 fn log_expected_side_effects(&self) {
515 match (&self.previous_state, &self.state) {
516 (Some(AppState::Command), AppState::Results) => {
517 debug!(target: "shadow_state",
518 "Expected side effects: Clear searches, reset viewport, enable nav keys");
519 }
520 (Some(AppState::Results), AppState::Search { .. }) => {
521 debug!(target: "shadow_state",
522 "Expected side effects: Clear other searches, setup search UI");
523 }
524 (Some(AppState::Search { .. }), AppState::Results) => {
525 debug!(target: "shadow_state",
526 "Expected side effects: Clear search UI, restore nav keys");
527 }
528 _ => {}
529 }
530 }
531}
532
533impl Default for ShadowStateManager {
534 fn default() -> Self {
535 Self::new()
536 }
537}