Skip to main content

fresh/app/
calibration_wizard.rs

1//! Input Calibration Wizard
2//!
3//! A fail-safe wizard for calibrating keyboard input in hostile terminal environments.
4//! Uses only lowercase ASCII letters for navigation (s, g, a, y, r) because they work
5//! on virtually every terminal since 1970.
6//!
7//! The wizard operates in two phases:
8//! 1. Capture Phase: User presses each target key, wizard records what the terminal sends
9//! 2. Verify Phase: User can test their mappings work correctly before saving
10
11use crate::input::key_translator::{KeyEventKey, KeyTranslator};
12use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
13use rust_i18n::t;
14use std::collections::{HashMap, HashSet};
15
16/// What the user's key SHOULD produce (the expected/normalized key)
17#[derive(Debug, Clone)]
18pub struct ExpectedKey {
19    pub code: KeyCode,
20    pub modifiers: KeyModifiers,
21}
22
23impl ExpectedKey {
24    pub const fn new(code: KeyCode, modifiers: KeyModifiers) -> Self {
25        Self { code, modifiers }
26    }
27
28    /// Convert to a KeyEvent for comparison
29    pub fn to_key_event(&self) -> KeyEvent {
30        KeyEvent::new(self.code, self.modifiers)
31    }
32}
33
34/// A single key that can be calibrated
35#[derive(Debug, Clone)]
36pub struct CalibrationTarget {
37    /// Display name for the key (e.g., "BACKSPACE", "CTRL+LEFT")
38    pub name: &'static str,
39    /// What Fresh expects to receive (the normalized key)
40    pub expected: ExpectedKey,
41}
42
43/// A group of related keys to calibrate
44#[derive(Debug, Clone)]
45pub struct CalibrationGroup {
46    /// Group name (e.g., "Basic Editing", "Line Navigation")
47    pub name: &'static str,
48    /// Keys in this group
49    pub targets: Vec<CalibrationTarget>,
50}
51
52/// Build the calibration groups as defined in the design document
53pub fn calibration_groups() -> Vec<CalibrationGroup> {
54    vec![
55        // Group 1: Basic Editing (4 keys)
56        CalibrationGroup {
57            name: "Basic Editing",
58            targets: vec![
59                CalibrationTarget {
60                    name: "BACKSPACE",
61                    expected: ExpectedKey::new(KeyCode::Backspace, KeyModifiers::NONE),
62                },
63                CalibrationTarget {
64                    name: "DELETE",
65                    expected: ExpectedKey::new(KeyCode::Delete, KeyModifiers::NONE),
66                },
67                CalibrationTarget {
68                    name: "TAB",
69                    expected: ExpectedKey::new(KeyCode::Tab, KeyModifiers::NONE),
70                },
71                CalibrationTarget {
72                    name: "SHIFT+TAB",
73                    expected: ExpectedKey::new(KeyCode::BackTab, KeyModifiers::SHIFT),
74                },
75            ],
76        },
77        // Group 2: Line Navigation (4 keys)
78        CalibrationGroup {
79            name: "Line Navigation",
80            targets: vec![
81                CalibrationTarget {
82                    name: "HOME",
83                    expected: ExpectedKey::new(KeyCode::Home, KeyModifiers::NONE),
84                },
85                CalibrationTarget {
86                    name: "END",
87                    expected: ExpectedKey::new(KeyCode::End, KeyModifiers::NONE),
88                },
89                CalibrationTarget {
90                    name: "SHIFT+HOME",
91                    expected: ExpectedKey::new(KeyCode::Home, KeyModifiers::SHIFT),
92                },
93                CalibrationTarget {
94                    name: "SHIFT+END",
95                    expected: ExpectedKey::new(KeyCode::End, KeyModifiers::SHIFT),
96                },
97            ],
98        },
99        // Group 3: Word Navigation (8 keys)
100        CalibrationGroup {
101            name: "Word Navigation",
102            targets: vec![
103                CalibrationTarget {
104                    name: "ALT+LEFT",
105                    expected: ExpectedKey::new(KeyCode::Left, KeyModifiers::ALT),
106                },
107                CalibrationTarget {
108                    name: "ALT+RIGHT",
109                    expected: ExpectedKey::new(KeyCode::Right, KeyModifiers::ALT),
110                },
111                CalibrationTarget {
112                    name: "ALT+SHIFT+LEFT",
113                    expected: ExpectedKey::new(
114                        KeyCode::Left,
115                        KeyModifiers::ALT.union(KeyModifiers::SHIFT),
116                    ),
117                },
118                CalibrationTarget {
119                    name: "ALT+SHIFT+RIGHT",
120                    expected: ExpectedKey::new(
121                        KeyCode::Right,
122                        KeyModifiers::ALT.union(KeyModifiers::SHIFT),
123                    ),
124                },
125                CalibrationTarget {
126                    name: "CTRL+LEFT",
127                    expected: ExpectedKey::new(KeyCode::Left, KeyModifiers::CONTROL),
128                },
129                CalibrationTarget {
130                    name: "CTRL+RIGHT",
131                    expected: ExpectedKey::new(KeyCode::Right, KeyModifiers::CONTROL),
132                },
133                CalibrationTarget {
134                    name: "CTRL+SHIFT+LEFT",
135                    expected: ExpectedKey::new(
136                        KeyCode::Left,
137                        KeyModifiers::CONTROL.union(KeyModifiers::SHIFT),
138                    ),
139                },
140                CalibrationTarget {
141                    name: "CTRL+SHIFT+RIGHT",
142                    expected: ExpectedKey::new(
143                        KeyCode::Right,
144                        KeyModifiers::CONTROL.union(KeyModifiers::SHIFT),
145                    ),
146                },
147            ],
148        },
149        // Group 4: Document Navigation (4 keys)
150        CalibrationGroup {
151            name: "Document Navigation",
152            targets: vec![
153                CalibrationTarget {
154                    name: "PAGE UP",
155                    expected: ExpectedKey::new(KeyCode::PageUp, KeyModifiers::NONE),
156                },
157                CalibrationTarget {
158                    name: "PAGE DOWN",
159                    expected: ExpectedKey::new(KeyCode::PageDown, KeyModifiers::NONE),
160                },
161                CalibrationTarget {
162                    name: "CTRL+HOME",
163                    expected: ExpectedKey::new(KeyCode::Home, KeyModifiers::CONTROL),
164                },
165                CalibrationTarget {
166                    name: "CTRL+END",
167                    expected: ExpectedKey::new(KeyCode::End, KeyModifiers::CONTROL),
168                },
169            ],
170        },
171        // Group 5: Emacs-Style Navigation (4 keys)
172        CalibrationGroup {
173            name: "Emacs-Style",
174            targets: vec![
175                CalibrationTarget {
176                    name: "CTRL+A",
177                    expected: ExpectedKey::new(KeyCode::Char('a'), KeyModifiers::CONTROL),
178                },
179                CalibrationTarget {
180                    name: "CTRL+E",
181                    expected: ExpectedKey::new(KeyCode::Char('e'), KeyModifiers::CONTROL),
182                },
183                CalibrationTarget {
184                    name: "CTRL+K",
185                    expected: ExpectedKey::new(KeyCode::Char('k'), KeyModifiers::CONTROL),
186                },
187                CalibrationTarget {
188                    name: "CTRL+Y",
189                    expected: ExpectedKey::new(KeyCode::Char('y'), KeyModifiers::CONTROL),
190                },
191            ],
192        },
193    ]
194}
195
196/// Current step in the calibration wizard
197#[derive(Debug, Clone)]
198pub enum CalibrationStep {
199    /// Capturing key for a specific target
200    Capture {
201        /// Index into calibration_groups()
202        group_idx: usize,
203        /// Index into group's targets list
204        key_idx: usize,
205    },
206    /// Verification phase - testing mapped keys
207    Verify,
208}
209
210/// Status of a single key calibration
211#[derive(Debug, Clone, PartialEq)]
212pub enum KeyStatus {
213    /// Not yet calibrated (waiting)
214    Pending,
215    /// Key was captured (different from expected)
216    Captured,
217    /// Key was skipped (using default)
218    Skipped,
219    /// Key was verified in verification phase
220    Verified,
221}
222
223/// Result of handling a key input
224#[derive(Debug, Clone)]
225pub enum WizardAction {
226    /// Continue to next key
227    Continue,
228    /// Go back to previous key
229    GoBack,
230    /// Skip to next group
231    SkipGroup,
232    /// Abort wizard (discard changes)
233    Abort,
234    /// Save and exit
235    Save,
236    /// Restart wizard
237    Restart,
238    /// Key was reserved, show message
239    ReservedKey,
240    /// Key captured, auto-advance
241    KeyCaptured,
242    /// Key verified in verification phase
243    KeyVerified,
244    /// Showing confirmation dialog
245    ShowConfirmation,
246}
247
248/// Pending confirmation for destructive actions
249#[derive(Debug, Clone, PartialEq)]
250pub enum PendingConfirmation {
251    /// No confirmation pending
252    None,
253    /// Confirming abort (discard all changes)
254    Abort,
255    /// Confirming restart (discard progress)
256    Restart,
257}
258
259/// The calibration wizard state machine
260#[derive(Debug)]
261pub struct CalibrationWizard {
262    /// Current step in the wizard
263    pub step: CalibrationStep,
264    /// Calibration groups (loaded once)
265    groups: Vec<CalibrationGroup>,
266    /// Pending translations: raw terminal event -> expected normalized event
267    pending_translations: HashMap<KeyEventKey, KeyEventKey>,
268    /// Status of each key (flattened index)
269    key_statuses: Vec<KeyStatus>,
270    /// Raw keys captured for each flat index (for undo when going back)
271    captured_raw_keys: HashMap<usize, KeyEventKey>,
272    /// Groups that were skipped entirely
273    skipped_groups: HashSet<usize>,
274    /// Which keys have been verified in verification phase
275    verified: HashSet<usize>,
276    /// Status message to display
277    pub status_message: Option<String>,
278    /// Pending confirmation dialog
279    pub pending_confirmation: PendingConfirmation,
280}
281
282impl CalibrationWizard {
283    /// Create a new calibration wizard
284    pub fn new() -> Self {
285        let groups = calibration_groups();
286        let total_keys: usize = groups.iter().map(|g| g.targets.len()).sum();
287
288        Self {
289            step: CalibrationStep::Capture {
290                group_idx: 0,
291                key_idx: 0,
292            },
293            groups,
294            pending_translations: HashMap::new(),
295            key_statuses: vec![KeyStatus::Pending; total_keys],
296            captured_raw_keys: HashMap::new(),
297            skipped_groups: HashSet::new(),
298            verified: HashSet::new(),
299            status_message: None,
300            pending_confirmation: PendingConfirmation::None,
301        }
302    }
303
304    /// Check if a confirmation dialog is pending
305    pub fn has_pending_confirmation(&self) -> bool {
306        self.pending_confirmation != PendingConfirmation::None
307    }
308
309    /// Handle key input when a confirmation dialog is showing
310    /// Uses action-based keys: 'd' for discard, 'r' for restart, 'k' for keep editing
311    pub fn handle_confirmation_key(&mut self, key: KeyEvent) -> WizardAction {
312        if key.modifiers != KeyModifiers::NONE {
313            return WizardAction::Continue;
314        }
315
316        match key.code {
317            KeyCode::Char('d') if self.pending_confirmation == PendingConfirmation::Abort => {
318                // 'd' confirms discard/abort
319                self.pending_confirmation = PendingConfirmation::None;
320                WizardAction::Abort
321            }
322            KeyCode::Char('r') if self.pending_confirmation == PendingConfirmation::Restart => {
323                // 'r' confirms restart
324                self.pending_confirmation = PendingConfirmation::None;
325                self.restart();
326                WizardAction::Restart
327            }
328            KeyCode::Char('c') | KeyCode::Esc => {
329                // 'c' or Esc cancels (keeps editing)
330                self.pending_confirmation = PendingConfirmation::None;
331                self.status_message = Some(t!("calibration.cancelled").to_string());
332                WizardAction::Continue
333            }
334            _ => WizardAction::Continue,
335        }
336    }
337
338    /// Get calibration groups
339    pub fn groups(&self) -> &[CalibrationGroup] {
340        &self.groups
341    }
342
343    /// Get key status by flattened index
344    pub fn key_status(&self, flat_idx: usize) -> &KeyStatus {
345        self.key_statuses
346            .get(flat_idx)
347            .unwrap_or(&KeyStatus::Pending)
348    }
349
350    /// Check if a group was skipped
351    pub fn is_group_skipped(&self, group_idx: usize) -> bool {
352        self.skipped_groups.contains(&group_idx)
353    }
354
355    /// Get the current target being calibrated (capture phase only)
356    pub fn current_target(&self) -> Option<(&CalibrationGroup, &CalibrationTarget, usize)> {
357        match &self.step {
358            CalibrationStep::Capture { group_idx, key_idx } => {
359                let group = self.groups.get(*group_idx)?;
360                let target = group.targets.get(*key_idx)?;
361                let flat_idx = self.flat_index(*group_idx, *key_idx);
362                Some((group, target, flat_idx))
363            }
364            CalibrationStep::Verify => None,
365        }
366    }
367
368    /// Get the current step number (1-indexed) and total
369    pub fn current_step_info(&self) -> (usize, usize) {
370        let total: usize = self.groups.iter().map(|g| g.targets.len()).sum();
371        match &self.step {
372            CalibrationStep::Capture { group_idx, key_idx } => {
373                let step = self.flat_index(*group_idx, *key_idx) + 1;
374                (step, total)
375            }
376            CalibrationStep::Verify => (total, total),
377        }
378    }
379
380    /// Convert (group_idx, key_idx) to flattened index
381    fn flat_index(&self, group_idx: usize, key_idx: usize) -> usize {
382        let mut idx = 0;
383        for (i, group) in self.groups.iter().enumerate() {
384            if i == group_idx {
385                return idx + key_idx;
386            }
387            idx += group.targets.len();
388        }
389        idx
390    }
391
392    /// Convert flattened index to (group_idx, key_idx)
393    #[allow(dead_code)]
394    fn unflat_index(&self, flat_idx: usize) -> Option<(usize, usize)> {
395        let mut idx = 0;
396        for (group_idx, group) in self.groups.iter().enumerate() {
397            if flat_idx < idx + group.targets.len() {
398                return Some((group_idx, flat_idx - idx));
399            }
400            idx += group.targets.len();
401        }
402        None
403    }
404
405    /// Handle a key event during capture phase
406    pub fn handle_capture_key(&mut self, key: KeyEvent) -> WizardAction {
407        let CalibrationStep::Capture { group_idx, key_idx } = &self.step else {
408            return WizardAction::Continue;
409        };
410
411        let group_idx = *group_idx;
412        let key_idx = *key_idx;
413
414        // Check for reserved control keys (lowercase letters without modifiers)
415        if key.modifiers == KeyModifiers::NONE {
416            match key.code {
417                KeyCode::Char('s') => {
418                    // Skip this key
419                    let flat_idx = self.flat_index(group_idx, key_idx);
420                    self.key_statuses[flat_idx] = KeyStatus::Skipped;
421                    self.status_message = Some(t!("calibration.skipped_key").to_string());
422                    self.advance_to_next();
423                    return WizardAction::Continue;
424                }
425                KeyCode::Char('b') => {
426                    // Go back to previous key
427                    if self.go_back() {
428                        return WizardAction::GoBack;
429                    } else {
430                        self.status_message = Some(t!("calibration.at_first_key").to_string());
431                        return WizardAction::Continue;
432                    }
433                }
434                KeyCode::Char('g') => {
435                    // Skip entire group
436                    self.skip_current_group();
437                    return WizardAction::SkipGroup;
438                }
439                KeyCode::Char('a') => {
440                    // Show confirmation before aborting
441                    self.pending_confirmation = PendingConfirmation::Abort;
442                    return WizardAction::ShowConfirmation;
443                }
444                KeyCode::Char('y') | KeyCode::Char('n') | KeyCode::Char('r') => {
445                    // Reserved for verification phase
446                    self.status_message = Some(t!("calibration.reserved_key").to_string());
447                    return WizardAction::ReservedKey;
448                }
449                _ => {}
450            }
451        }
452
453        // Capture the key
454        let flat_idx = self.flat_index(group_idx, key_idx);
455        let target = &self.groups[group_idx].targets[key_idx];
456        let expected = target.expected.to_key_event();
457
458        // Check if the key is already what we expect (no translation needed)
459        if key.code == expected.code && key.modifiers == expected.modifiers {
460            self.key_statuses[flat_idx] = KeyStatus::Skipped;
461            self.captured_raw_keys.remove(&flat_idx);
462            self.status_message = Some(t!("calibration.key_works").to_string());
463        } else {
464            // Record the translation: raw -> expected
465            let raw_key = KeyEventKey::from_key_event(&key);
466            let expected_key = KeyEventKey::from_key_event(&expected);
467            self.pending_translations
468                .insert(raw_key.clone(), expected_key);
469            self.captured_raw_keys.insert(flat_idx, raw_key);
470            self.key_statuses[flat_idx] = KeyStatus::Captured;
471            self.status_message = Some(
472                t!(
473                    "calibration.captured",
474                    key = format!("{:?}", key.code),
475                    target = target.name
476                )
477                .to_string(),
478            );
479        }
480
481        self.advance_to_next();
482        WizardAction::KeyCaptured
483    }
484
485    /// Handle a key event during verification phase
486    pub fn handle_verify_key(&mut self, key: KeyEvent) -> WizardAction {
487        // Check for control keys
488        if key.modifiers == KeyModifiers::NONE {
489            match key.code {
490                KeyCode::Char('y') => {
491                    return WizardAction::Save;
492                }
493                KeyCode::Char('r') => {
494                    // Show confirmation before restarting
495                    self.pending_confirmation = PendingConfirmation::Restart;
496                    return WizardAction::ShowConfirmation;
497                }
498                KeyCode::Char('a') => {
499                    // Show confirmation before aborting
500                    self.pending_confirmation = PendingConfirmation::Abort;
501                    return WizardAction::ShowConfirmation;
502                }
503                KeyCode::Char('b') => {
504                    // Go back to last capture key from verify phase
505                    if self.go_back() {
506                        return WizardAction::GoBack;
507                    }
508                    return WizardAction::Continue;
509                }
510                _ => {}
511            }
512        }
513
514        // Try to find a matching expected key
515        // Apply translation first
516        let translated = self.translate_key(key);
517
518        // Find which target this matches
519        for (group_idx, group) in self.groups.iter().enumerate() {
520            if self.skipped_groups.contains(&group_idx) {
521                continue;
522            }
523            for (key_idx, target) in group.targets.iter().enumerate() {
524                let expected = target.expected.to_key_event();
525                if translated.code == expected.code && translated.modifiers == expected.modifiers {
526                    let flat_idx = self.flat_index(group_idx, key_idx);
527                    self.verified.insert(flat_idx);
528                    self.key_statuses[flat_idx] = KeyStatus::Verified;
529                    self.status_message =
530                        Some(t!("calibration.key_verified", key = target.name).to_string());
531                    return WizardAction::KeyVerified;
532                }
533            }
534        }
535
536        self.status_message = Some(t!("calibration.key_not_recognized").to_string());
537        WizardAction::Continue
538    }
539
540    /// Translate a key using pending translations
541    fn translate_key(&self, key: KeyEvent) -> KeyEvent {
542        let raw_key = KeyEventKey::from_key_event(&key);
543        if let Some(expected_key) = self.pending_translations.get(&raw_key) {
544            expected_key.to_key_event()
545        } else {
546            key
547        }
548    }
549
550    /// Skip the current group
551    fn skip_current_group(&mut self) {
552        if let CalibrationStep::Capture { group_idx, key_idx } = &self.step {
553            let group_idx = *group_idx;
554            let key_idx = *key_idx;
555
556            // Mark all remaining keys in this group as skipped
557            let group = &self.groups[group_idx];
558            for i in key_idx..group.targets.len() {
559                let flat_idx = self.flat_index(group_idx, i);
560                self.key_statuses[flat_idx] = KeyStatus::Skipped;
561            }
562
563            self.skipped_groups.insert(group_idx);
564            self.status_message =
565                Some(t!("calibration.skipped_group", group = group.name).to_string());
566
567            // Advance to next group
568            if group_idx + 1 < self.groups.len() {
569                self.step = CalibrationStep::Capture {
570                    group_idx: group_idx + 1,
571                    key_idx: 0,
572                };
573            } else {
574                self.step = CalibrationStep::Verify;
575            }
576        }
577    }
578
579    /// Advance to the next key/group/phase
580    fn advance_to_next(&mut self) {
581        if let CalibrationStep::Capture { group_idx, key_idx } = &self.step {
582            let group_idx = *group_idx;
583            let key_idx = *key_idx;
584
585            let group = &self.groups[group_idx];
586            if key_idx + 1 < group.targets.len() {
587                // Next key in same group
588                self.step = CalibrationStep::Capture {
589                    group_idx,
590                    key_idx: key_idx + 1,
591                };
592            } else if group_idx + 1 < self.groups.len() {
593                // First key in next group
594                self.step = CalibrationStep::Capture {
595                    group_idx: group_idx + 1,
596                    key_idx: 0,
597                };
598            } else {
599                // All keys captured, move to verification
600                self.step = CalibrationStep::Verify;
601                self.status_message = Some(t!("calibration.capture_complete").to_string());
602            }
603        }
604    }
605
606    /// Go back to the previous key, undoing any capture
607    /// Returns true if we went back, false if already at the first key
608    fn go_back(&mut self) -> bool {
609        let (group_idx, key_idx) = match &self.step {
610            CalibrationStep::Capture { group_idx, key_idx } => (*group_idx, *key_idx),
611            CalibrationStep::Verify => {
612                // Go back to the last key
613                let last_group = self.groups.len() - 1;
614                let last_key = self.groups[last_group].targets.len() - 1;
615                self.step = CalibrationStep::Capture {
616                    group_idx: last_group,
617                    key_idx: last_key,
618                };
619                self.undo_key_at(last_group, last_key);
620                self.status_message = Some(t!("calibration.went_back").to_string());
621                return true;
622            }
623        };
624
625        // Already at the first key?
626        if group_idx == 0 && key_idx == 0 {
627            return false;
628        }
629
630        // Calculate previous position
631        let (prev_group, prev_key) = if key_idx > 0 {
632            (group_idx, key_idx - 1)
633        } else {
634            // Go to last key of previous group
635            let prev_group = group_idx - 1;
636            let prev_key = self.groups[prev_group].targets.len() - 1;
637            // Un-skip the group if we're going back into it
638            self.skipped_groups.remove(&prev_group);
639            (prev_group, prev_key)
640        };
641
642        self.step = CalibrationStep::Capture {
643            group_idx: prev_group,
644            key_idx: prev_key,
645        };
646        self.undo_key_at(prev_group, prev_key);
647        self.status_message = Some(t!("calibration.went_back").to_string());
648        true
649    }
650
651    /// Undo the capture at the given position
652    fn undo_key_at(&mut self, group_idx: usize, key_idx: usize) {
653        let flat_idx = self.flat_index(group_idx, key_idx);
654
655        // Remove any translation we recorded for this key
656        if let Some(raw_key) = self.captured_raw_keys.remove(&flat_idx) {
657            self.pending_translations.remove(&raw_key);
658        }
659
660        // Reset status to pending
661        self.key_statuses[flat_idx] = KeyStatus::Pending;
662    }
663
664    /// Reset the wizard to start over
665    pub fn restart(&mut self) {
666        let total_keys: usize = self.groups.iter().map(|g| g.targets.len()).sum();
667        self.step = CalibrationStep::Capture {
668            group_idx: 0,
669            key_idx: 0,
670        };
671        self.pending_translations.clear();
672        self.key_statuses = vec![KeyStatus::Pending; total_keys];
673        self.captured_raw_keys.clear();
674        self.skipped_groups.clear();
675        self.verified.clear();
676        self.status_message = Some(t!("calibration.restarted").to_string());
677    }
678
679    /// Check if we're in verify phase
680    pub fn is_verify_phase(&self) -> bool {
681        matches!(self.step, CalibrationStep::Verify)
682    }
683
684    /// Get the number of translations captured
685    pub fn translation_count(&self) -> usize {
686        self.pending_translations.len()
687    }
688
689    /// Build a KeyTranslator from the pending translations
690    pub fn build_translator(&self) -> KeyTranslator {
691        let mut translator = KeyTranslator::new();
692        for (raw, expected) in &self.pending_translations {
693            translator.add_translation(raw.to_key_event(), expected.to_key_event());
694        }
695        translator
696    }
697
698    /// Get verification progress (verified, total)
699    pub fn verification_progress(&self) -> (usize, usize) {
700        let total: usize = self
701            .key_statuses
702            .iter()
703            .filter(|s| matches!(s, KeyStatus::Captured))
704            .count();
705        let verified = self.verified.len();
706        (verified, total)
707    }
708
709    /// Get all key statuses with their group/key info
710    pub fn all_key_info(&self) -> Vec<(usize, usize, &CalibrationTarget, &KeyStatus)> {
711        let mut result = Vec::new();
712        let mut flat_idx = 0;
713        for (group_idx, group) in self.groups.iter().enumerate() {
714            for (key_idx, target) in group.targets.iter().enumerate() {
715                let status = &self.key_statuses[flat_idx];
716                result.push((group_idx, key_idx, target, status));
717                flat_idx += 1;
718            }
719        }
720        result
721    }
722}
723
724impl Default for CalibrationWizard {
725    fn default() -> Self {
726        Self::new()
727    }
728}
729
730#[cfg(test)]
731mod tests {
732    use super::*;
733
734    #[test]
735    fn test_wizard_creation() {
736        let wizard = CalibrationWizard::new();
737        assert!(matches!(
738            wizard.step,
739            CalibrationStep::Capture {
740                group_idx: 0,
741                key_idx: 0
742            }
743        ));
744        assert_eq!(wizard.translation_count(), 0);
745    }
746
747    #[test]
748    fn test_step_info() {
749        let wizard = CalibrationWizard::new();
750        let (step, total) = wizard.current_step_info();
751        assert_eq!(step, 1);
752        assert_eq!(total, 24); // 4 + 4 + 8 + 4 + 4 = 24 keys
753    }
754
755    #[test]
756    fn test_skip_key() {
757        let mut wizard = CalibrationWizard::new();
758
759        // Skip first key with 's'
760        let key = KeyEvent::new(KeyCode::Char('s'), KeyModifiers::NONE);
761        let action = wizard.handle_capture_key(key);
762
763        assert!(matches!(action, WizardAction::Continue));
764        assert_eq!(*wizard.key_status(0), KeyStatus::Skipped);
765
766        // Should have advanced to next key
767        assert!(matches!(
768            wizard.step,
769            CalibrationStep::Capture {
770                group_idx: 0,
771                key_idx: 1
772            }
773        ));
774    }
775
776    #[test]
777    fn test_skip_group() {
778        let mut wizard = CalibrationWizard::new();
779
780        // Skip group with 'g'
781        let key = KeyEvent::new(KeyCode::Char('g'), KeyModifiers::NONE);
782        let action = wizard.handle_capture_key(key);
783
784        assert!(matches!(action, WizardAction::SkipGroup));
785        assert!(wizard.is_group_skipped(0));
786
787        // Should have advanced to next group
788        assert!(matches!(
789            wizard.step,
790            CalibrationStep::Capture {
791                group_idx: 1,
792                key_idx: 0
793            }
794        ));
795    }
796
797    #[test]
798    fn test_abort_with_confirmation() {
799        let mut wizard = CalibrationWizard::new();
800
801        // Press 'a' to request abort
802        let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
803        let action = wizard.handle_capture_key(key);
804
805        // Should show confirmation first
806        assert!(matches!(action, WizardAction::ShowConfirmation));
807        assert!(wizard.has_pending_confirmation());
808        assert!(matches!(
809            wizard.pending_confirmation,
810            PendingConfirmation::Abort
811        ));
812
813        // Confirm with 'd' for discard
814        let confirm_key = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE);
815        let action = wizard.handle_confirmation_key(confirm_key);
816
817        assert!(matches!(action, WizardAction::Abort));
818    }
819
820    #[test]
821    fn test_abort_cancelled() {
822        let mut wizard = CalibrationWizard::new();
823
824        // Press 'a' to request abort
825        let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
826        wizard.handle_capture_key(key);
827
828        // Cancel with 'c'
829        let cancel_key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE);
830        let action = wizard.handle_confirmation_key(cancel_key);
831
832        assert!(matches!(action, WizardAction::Continue));
833        assert!(!wizard.has_pending_confirmation());
834    }
835
836    #[test]
837    fn test_go_back() {
838        let mut wizard = CalibrationWizard::new();
839
840        // Capture first key (Backspace)
841        let backspace = KeyEvent::new(KeyCode::Char('\x7f'), KeyModifiers::NONE);
842        wizard.handle_capture_key(backspace);
843
844        // We should be at key 1 now (DELETE)
845        assert!(matches!(
846            wizard.step,
847            CalibrationStep::Capture {
848                group_idx: 0,
849                key_idx: 1
850            }
851        ));
852        assert_eq!(wizard.translation_count(), 1);
853
854        // Press 'b' to go back
855        let go_back = KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE);
856        let action = wizard.handle_capture_key(go_back);
857
858        assert!(matches!(action, WizardAction::GoBack));
859
860        // Should be back at key 0 (BACKSPACE)
861        assert!(matches!(
862            wizard.step,
863            CalibrationStep::Capture {
864                group_idx: 0,
865                key_idx: 0
866            }
867        ));
868
869        // Translation should be undone
870        assert_eq!(wizard.translation_count(), 0);
871        assert_eq!(*wizard.key_status(0), KeyStatus::Pending);
872    }
873
874    #[test]
875    fn test_go_back_at_first_key() {
876        let mut wizard = CalibrationWizard::new();
877
878        // Try to go back at first key
879        let go_back = KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE);
880        let action = wizard.handle_capture_key(go_back);
881
882        // Should stay at first key
883        assert!(matches!(action, WizardAction::Continue));
884        assert!(matches!(
885            wizard.step,
886            CalibrationStep::Capture {
887                group_idx: 0,
888                key_idx: 0
889            }
890        ));
891    }
892
893    #[test]
894    fn test_reserved_key() {
895        let mut wizard = CalibrationWizard::new();
896
897        // 'y' is reserved
898        let key = KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE);
899        let action = wizard.handle_capture_key(key);
900
901        assert!(matches!(action, WizardAction::ReservedKey));
902    }
903
904    #[test]
905    fn test_capture_key() {
906        let mut wizard = CalibrationWizard::new();
907
908        // Simulate a terminal sending 0x7F for backspace
909        let key = KeyEvent::new(KeyCode::Char('\x7f'), KeyModifiers::NONE);
910        let action = wizard.handle_capture_key(key);
911
912        assert!(matches!(action, WizardAction::KeyCaptured));
913        assert_eq!(*wizard.key_status(0), KeyStatus::Captured);
914        assert_eq!(wizard.translation_count(), 1);
915    }
916
917    #[test]
918    fn test_capture_correct_key() {
919        let mut wizard = CalibrationWizard::new();
920
921        // Send the correct key (Backspace)
922        let key = KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE);
923        let action = wizard.handle_capture_key(key);
924
925        assert!(matches!(action, WizardAction::KeyCaptured));
926        // No translation needed, marked as skipped
927        assert_eq!(*wizard.key_status(0), KeyStatus::Skipped);
928        assert_eq!(wizard.translation_count(), 0);
929    }
930
931    #[test]
932    fn test_restart() {
933        let mut wizard = CalibrationWizard::new();
934
935        // Capture a key
936        let key = KeyEvent::new(KeyCode::Char('\x7f'), KeyModifiers::NONE);
937        wizard.handle_capture_key(key);
938
939        assert_eq!(wizard.translation_count(), 1);
940
941        // Restart
942        wizard.restart();
943
944        assert_eq!(wizard.translation_count(), 0);
945        assert!(matches!(
946            wizard.step,
947            CalibrationStep::Capture {
948                group_idx: 0,
949                key_idx: 0
950            }
951        ));
952    }
953
954    #[test]
955    fn test_verify_phase() {
956        let mut wizard = CalibrationWizard::new();
957
958        // Skip all keys to get to verify phase
959        for _ in 0..24 {
960            let key = KeyEvent::new(KeyCode::Char('s'), KeyModifiers::NONE);
961            wizard.handle_capture_key(key);
962        }
963
964        assert!(wizard.is_verify_phase());
965    }
966
967    #[test]
968    fn test_verify_save() {
969        let mut wizard = CalibrationWizard::new();
970        wizard.step = CalibrationStep::Verify;
971
972        let key = KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE);
973        let action = wizard.handle_verify_key(key);
974
975        assert!(matches!(action, WizardAction::Save));
976    }
977
978    #[test]
979    fn test_build_translator() {
980        let mut wizard = CalibrationWizard::new();
981
982        // Capture a key mapping
983        let raw = KeyEvent::new(KeyCode::Char('\x7f'), KeyModifiers::NONE);
984        wizard.handle_capture_key(raw);
985
986        let translator = wizard.build_translator();
987        assert_eq!(translator.len(), 1);
988
989        // Test the translation
990        let translated = translator.translate(raw);
991        assert_eq!(translated.code, KeyCode::Backspace);
992    }
993}