use crate::buffer::{AppMode, Buffer, BufferAPI};
use std::collections::VecDeque;
use std::time::Instant;
use tracing::{debug, info, warn};
#[derive(Debug, Clone, PartialEq)]
pub enum AppState {
Command,
Results,
Search { search_type: SearchType },
Help,
Debug,
History,
JumpToRow,
ColumnStats,
}
#[derive(Debug, Clone, PartialEq)]
pub enum SearchType {
Vim, Column, Data, Fuzzy, }
pub struct ShadowStateManager {
state: AppState,
previous_state: Option<AppState>,
history: VecDeque<StateTransition>,
transition_count: usize,
discrepancies: Vec<String>,
}
#[derive(Debug, Clone)]
struct StateTransition {
timestamp: Instant,
from: AppState,
to: AppState,
trigger: String,
}
impl ShadowStateManager {
pub fn new() -> Self {
info!(target: "shadow_state", "Shadow state manager initialized");
Self {
state: AppState::Command,
previous_state: None,
history: VecDeque::with_capacity(100),
transition_count: 0,
discrepancies: Vec::new(),
}
}
pub fn observe_mode_change(&mut self, mode: AppMode, trigger: &str) {
let new_state = self.mode_to_state(mode.clone());
if new_state == self.state {
debug!(target: "shadow_state",
"Redundant mode change to {:?} ignored", mode);
} else {
self.transition_count += 1;
info!(target: "shadow_state",
"[#{}] {} -> {} (trigger: {})",
self.transition_count,
self.state_display(&self.state),
self.state_display(&new_state),
trigger
);
let transition = StateTransition {
timestamp: Instant::now(),
from: self.state.clone(),
to: new_state.clone(),
trigger: trigger.to_string(),
};
self.history.push_back(transition);
if self.history.len() > 100 {
self.history.pop_front();
}
self.previous_state = Some(self.state.clone());
self.state = new_state;
self.log_expected_side_effects();
}
}
pub fn set_mode(&mut self, mode: AppMode, buffer: &mut Buffer, trigger: &str) {
let new_state = self.mode_to_state(mode.clone());
if new_state == self.state {
debug!(target: "shadow_state",
"Redundant mode change to {:?} ignored", mode);
} else {
self.transition_count += 1;
info!(target: "shadow_state",
"[#{}] {} -> {} (trigger: {})",
self.transition_count,
self.state_display(&self.state),
self.state_display(&new_state),
trigger
);
let transition = StateTransition {
timestamp: Instant::now(),
from: self.state.clone(),
to: new_state.clone(),
trigger: trigger.to_string(),
};
self.history.push_back(transition);
if self.history.len() > 100 {
self.history.pop_front();
}
self.previous_state = Some(self.state.clone());
self.state = new_state;
buffer.set_mode(mode);
self.log_expected_side_effects();
}
}
pub fn switch_to_results(&mut self, buffer: &mut Buffer) {
self.set_mode(AppMode::Results, buffer, "switch_to_results");
}
pub fn switch_to_command(&mut self, buffer: &mut Buffer) {
self.set_mode(AppMode::Command, buffer, "switch_to_command");
}
pub fn start_search(&mut self, search_type: SearchType, buffer: &mut Buffer, trigger: &str) {
let mode = match search_type {
SearchType::Column => AppMode::ColumnSearch,
SearchType::Data | SearchType::Vim => AppMode::Search,
SearchType::Fuzzy => AppMode::FuzzyFilter,
};
self.state = AppState::Search {
search_type: search_type.clone(),
};
self.transition_count += 1;
info!(target: "shadow_state",
"[#{}] Starting {:?} search (trigger: {})",
self.transition_count, search_type, trigger
);
buffer.set_mode(mode);
}
pub fn exit_to_results(&mut self, buffer: &mut Buffer) {
self.set_mode(AppMode::Results, buffer, "exit_to_results");
}
pub fn observe_search_start(&mut self, search_type: SearchType, trigger: &str) {
let new_state = AppState::Search {
search_type: search_type.clone(),
};
if !matches!(self.state, AppState::Search { .. }) {
self.transition_count += 1;
info!(target: "shadow_state",
"[#{}] {} -> {:?} search (trigger: {})",
self.transition_count,
self.state_display(&self.state),
search_type,
trigger
);
self.previous_state = Some(self.state.clone());
self.state = new_state;
warn!(target: "shadow_state",
"⚠️ Search started - verify other search states were cleared!");
}
}
pub fn observe_search_end(&mut self, trigger: &str) {
if matches!(self.state, AppState::Search { .. }) {
let new_state = AppState::Results;
info!(target: "shadow_state",
"[#{}] Exiting search -> {} (trigger: {})",
self.transition_count,
self.state_display(&new_state),
trigger
);
self.previous_state = Some(self.state.clone());
self.state = new_state;
info!(target: "shadow_state",
"✓ Expected side effects: Clear search UI, restore navigation keys");
}
}
#[must_use]
pub fn is_search_active(&self) -> bool {
matches!(self.state, AppState::Search { .. })
}
#[must_use]
pub fn get_search_type(&self) -> Option<SearchType> {
if let AppState::Search { ref search_type } = self.state {
Some(search_type.clone())
} else {
None
}
}
#[must_use]
pub fn status_display(&self) -> String {
format!("[Shadow: {}]", self.state_display(&self.state))
}
#[must_use]
pub fn debug_info(&self) -> String {
let mut info = format!(
"Shadow State Debug (transitions: {})\n",
self.transition_count
);
info.push_str(&format!("Current: {:?}\n", self.state));
if !self.history.is_empty() {
info.push_str("\nRecent transitions:\n");
for transition in self.history.iter().rev().take(5) {
info.push_str(&format!(
" {:?} ago: {} -> {} ({})\n",
transition.timestamp.elapsed(),
self.state_display(&transition.from),
self.state_display(&transition.to),
transition.trigger
));
}
}
if !self.discrepancies.is_empty() {
info.push_str("\n⚠️ Discrepancies detected:\n");
for disc in self.discrepancies.iter().rev().take(3) {
info.push_str(&format!(" - {disc}\n"));
}
}
info
}
pub fn report_discrepancy(&mut self, expected: &str, actual: &str) {
let msg = format!("Expected: {expected}, Actual: {actual}");
warn!(target: "shadow_state", "Discrepancy: {}", msg);
self.discrepancies.push(msg);
}
#[must_use]
pub fn get_state(&self) -> &AppState {
&self.state
}
#[must_use]
pub fn get_mode(&self) -> AppMode {
match &self.state {
AppState::Command => AppMode::Command,
AppState::Results => AppMode::Results,
AppState::Search { search_type } => match search_type {
SearchType::Column => AppMode::ColumnSearch,
SearchType::Data => AppMode::Search,
SearchType::Fuzzy => AppMode::FuzzyFilter,
SearchType::Vim => AppMode::Search, },
AppState::Help => AppMode::Help,
AppState::Debug => AppMode::Debug,
AppState::History => AppMode::History,
AppState::JumpToRow => AppMode::JumpToRow,
AppState::ColumnStats => AppMode::ColumnStats,
}
}
#[must_use]
pub fn is_in_results_mode(&self) -> bool {
matches!(self.state, AppState::Results)
}
#[must_use]
pub fn is_in_command_mode(&self) -> bool {
matches!(self.state, AppState::Command)
}
#[must_use]
pub fn is_in_search_mode(&self) -> bool {
matches!(self.state, AppState::Search { .. })
}
#[must_use]
pub fn is_in_help_mode(&self) -> bool {
matches!(self.state, AppState::Help)
}
#[must_use]
pub fn is_in_debug_mode(&self) -> bool {
matches!(self.state, AppState::Debug)
}
#[must_use]
pub fn is_in_history_mode(&self) -> bool {
matches!(self.state, AppState::History)
}
#[must_use]
pub fn is_in_jump_mode(&self) -> bool {
matches!(self.state, AppState::JumpToRow)
}
#[must_use]
pub fn is_in_column_stats_mode(&self) -> bool {
matches!(self.state, AppState::ColumnStats)
}
#[must_use]
pub fn is_in_column_search(&self) -> bool {
matches!(
self.state,
AppState::Search {
search_type: SearchType::Column
}
)
}
#[must_use]
pub fn is_in_data_search(&self) -> bool {
matches!(
self.state,
AppState::Search {
search_type: SearchType::Data
}
)
}
#[must_use]
pub fn is_in_fuzzy_filter(&self) -> bool {
matches!(
self.state,
AppState::Search {
search_type: SearchType::Fuzzy
}
)
}
#[must_use]
pub fn is_in_vim_search(&self) -> bool {
matches!(
self.state,
AppState::Search {
search_type: SearchType::Vim
}
)
}
#[must_use]
pub fn get_previous_state(&self) -> Option<&AppState> {
self.previous_state.as_ref()
}
#[must_use]
pub fn can_navigate(&self) -> bool {
self.is_in_results_mode()
}
#[must_use]
pub fn can_edit(&self) -> bool {
self.is_in_command_mode() || self.is_in_search_mode()
}
#[must_use]
pub fn get_transition_count(&self) -> usize {
self.transition_count
}
#[must_use]
pub fn get_last_transition(&self) -> Option<&StateTransition> {
self.history.back()
}
fn mode_to_state(&self, mode: AppMode) -> AppState {
match mode {
AppMode::Command => AppState::Command,
AppMode::Results => AppState::Results,
AppMode::Search | AppMode::ColumnSearch => {
if let AppState::Search { ref search_type } = self.state {
AppState::Search {
search_type: search_type.clone(),
}
} else {
let search_type = match mode {
AppMode::ColumnSearch => SearchType::Column,
_ => SearchType::Data,
};
AppState::Search { search_type }
}
}
AppMode::Help => AppState::Help,
AppMode::Debug | AppMode::PrettyQuery => AppState::Debug,
AppMode::History => AppState::History,
AppMode::JumpToRow => AppState::JumpToRow,
AppMode::ColumnStats => AppState::ColumnStats,
_ => self.state.clone(), }
}
fn state_display(&self, state: &AppState) -> String {
match state {
AppState::Command => "COMMAND".to_string(),
AppState::Results => "RESULTS".to_string(),
AppState::Search { search_type } => format!("SEARCH({search_type:?})"),
AppState::Help => "HELP".to_string(),
AppState::Debug => "DEBUG".to_string(),
AppState::History => "HISTORY".to_string(),
AppState::JumpToRow => "JUMP_TO_ROW".to_string(),
AppState::ColumnStats => "COLUMN_STATS".to_string(),
}
}
fn log_expected_side_effects(&self) {
match (&self.previous_state, &self.state) {
(Some(AppState::Command), AppState::Results) => {
debug!(target: "shadow_state",
"Expected side effects: Clear searches, reset viewport, enable nav keys");
}
(Some(AppState::Results), AppState::Search { .. }) => {
debug!(target: "shadow_state",
"Expected side effects: Clear other searches, setup search UI");
}
(Some(AppState::Search { .. }), AppState::Results) => {
debug!(target: "shadow_state",
"Expected side effects: Clear search UI, restore nav keys");
}
_ => {}
}
}
}
impl Default for ShadowStateManager {
fn default() -> Self {
Self::new()
}
}