Skip to main content

chess_tui/
handler.rs

1use crate::constants::{Popups, BOT_DIFFICULTY_COUNT};
2use crate::game_logic::coord::Coord;
3use crate::game_logic::game::GameState;
4use crate::utils::{flip_square_if_needed, get_coord_from_square, normalize_lowercase_to_san};
5use crate::{
6    app::{App, AppResult},
7    constants::Pages,
8};
9use ratatui::crossterm::event::{
10    KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
11};
12use shakmaty::san::San;
13use shakmaty::{Role, Square};
14
15/// Handles keyboard input events and updates the application state accordingly.
16///
17/// This is the main entry point for all keyboard interactions. It filters out
18/// non-press events, handles mouse-to-keyboard transitions, and routes events
19/// to either popup handlers or page handlers based on the current application state.
20pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
21    // Only process key press events, ignore release and repeat events
22    // (crossterm on Windows sends Release and Repeat events as well)
23    if key_event.kind != KeyEventKind::Press {
24        return Ok(());
25    }
26
27    // Reset cursor position after mouse interaction when keyboard is used
28    // This ensures the cursor is in a valid position for keyboard navigation
29    if app.game.ui.mouse_used {
30        app.game.ui.mouse_used = false;
31        if let Some(selected_square) = app.game.ui.selected_square {
32            // If a piece was selected via mouse, move cursor to that square
33            app.game.ui.cursor_coordinates =
34                get_coord_from_square(Some(selected_square), app.game.logic.game_board.is_flipped);
35            app.game.ui.selected_square = None;
36        } else {
37            // Otherwise, reset cursor to center of board (e4/e5)
38            app.game.ui.cursor_coordinates.col = 4;
39            app.game.ui.cursor_coordinates.row = 4;
40        }
41    }
42
43    // If a popup is active, the key should affect the popup and not the page,
44    // therefore, if there is some popup active, handle it, if not, handle the page event
45    match app.current_popup {
46        Some(popup) => handle_popup_input(app, key_event, popup),
47        None => handle_page_input(app, key_event),
48    }
49
50    Ok(())
51}
52
53/// Handles keyboard input when a popup is active.
54///
55/// Popups take priority over page input, so all keyboard events are routed here
56/// when a popup is displayed. Each popup type has its own set of key bindings.
57fn handle_popup_input(app: &mut App, key_event: KeyEvent, popup: Popups) {
58    match popup {
59        // Popup for entering the host IP address when joining a multiplayer game
60        Popups::EnterHostIP => match key_event.code {
61            KeyCode::Enter => {
62                // Submit the entered IP address and store it
63                app.game.ui.prompt.submit_message();
64                if app.current_page == Pages::Multiplayer {
65                    app.host_ip = Some(app.game.ui.prompt.message.clone());
66                }
67                app.current_popup = None;
68            }
69            KeyCode::Char(to_insert) => app.game.ui.prompt.enter_char(to_insert),
70            KeyCode::Backspace => app.game.ui.prompt.delete_char(),
71            KeyCode::Left => app.game.ui.prompt.move_cursor_left(),
72            KeyCode::Right => app.game.ui.prompt.move_cursor_right(),
73            KeyCode::Esc => {
74                // Cancel IP entry and return to home menu, resetting multiplayer state
75                app.current_popup = None;
76                if app.current_page == Pages::Multiplayer {
77                    app.hosting = None;
78                    app.selected_color = None;
79                    app.menu_cursor = 0;
80                }
81                app.current_page = Pages::Home;
82            }
83            _ => fallback_key_handler(app, key_event),
84        },
85        // Help popup - shows game controls and key bindings
86        Popups::Help => match key_event.code {
87            KeyCode::Char('?') => app.toggle_help_popup(),
88            KeyCode::Esc => app.toggle_help_popup(),
89            _ => fallback_key_handler(app, key_event),
90        },
91        // Multiplayer selection popup - choose to host or join a game
92        Popups::MultiplayerSelection => match key_event.code {
93            KeyCode::Esc => app.close_popup_and_go_home(),
94            KeyCode::Right | KeyCode::Char('l') => app.menu_cursor_right(2),
95            KeyCode::Left | KeyCode::Char('h') => app.menu_cursor_left(2),
96            KeyCode::Char(' ') | KeyCode::Enter => app.hosting_selection(),
97            _ => fallback_key_handler(app, key_event),
98        },
99        Popups::EnginePathError => match key_event.code {
100            KeyCode::Esc | KeyCode::Enter | KeyCode::Char(' ') => app.close_popup_and_go_home(),
101            _ => fallback_key_handler(app, key_event),
102        },
103        Popups::WaitingForOpponentToJoin => match key_event.code {
104            KeyCode::Esc | KeyCode::Enter | KeyCode::Char(' ') => {
105                app.close_popup_and_go_home();
106                app.cancel_hosting_cleanup();
107            }
108            _ => fallback_key_handler(app, key_event),
109        },
110        // End screen popup - shown when game ends (checkmate or draw)
111        Popups::EndScreen => match key_event.code {
112            KeyCode::Char('h' | 'H') => {
113                // Hide the end screen (can be toggled back with H when not in popup)
114                app.current_popup = None;
115                app.end_screen_dismissed = true;
116            }
117            KeyCode::Esc => {
118                // Also allow Esc to hide the end screen and mark as dismissed
119                app.current_popup = None;
120                app.end_screen_dismissed = true;
121            }
122            KeyCode::Char('r' | 'R') => {
123                // Restart the game (only for non-multiplayer games)
124                if app.game.logic.opponent.is_none() {
125                    app.restart();
126                    app.current_popup = None;
127                }
128            }
129            KeyCode::Char('b' | 'B') => {
130                // Go back to home menu - completely reset all game state
131                app.reset_home();
132            }
133            _ => fallback_key_handler(app, key_event),
134        },
135        // Puzzle end screen popup - shown when puzzle is completed
136        Popups::PuzzleEndScreen => match key_event.code {
137            KeyCode::Char('h' | 'H') => {
138                // Hide the puzzle end screen
139                app.current_popup = None;
140            }
141            KeyCode::Esc => {
142                // Also allow Esc to hide the puzzle end screen
143                app.current_popup = None;
144            }
145            KeyCode::Char('n' | 'N') => {
146                // Start a new puzzle
147                app.current_popup = None;
148                app.start_puzzle_mode();
149            }
150            KeyCode::Char('b' | 'B') => {
151                // Go back to home menu - completely reset all game state
152                app.reset_home();
153            }
154            _ => fallback_key_handler(app, key_event),
155        },
156        // Error popup - displays error messages
157        Popups::Error => match key_event.code {
158            KeyCode::Esc | KeyCode::Enter | KeyCode::Char(' ') => {
159                app.current_popup = None;
160                app.error_message = None;
161                // Navigate back to an appropriate page based on current context
162                match app.current_page {
163                    Pages::Lichess | Pages::OngoingGames => {
164                        // If we're on Lichess-related pages, go back to Lichess menu
165                        app.current_page = Pages::LichessMenu;
166                    }
167                    Pages::Multiplayer | Pages::Bot => {
168                        // If we're on multiplayer or bot page, go back to home
169                        app.current_page = Pages::Home;
170                    }
171                    _ => {
172                        // For other pages, stay on current page or go to home
173                        // Only change if we're in a weird state
174                        if app.current_page == Pages::Solo && app.game.logic.opponent.is_some() {
175                            // If we're in solo but have an opponent (shouldn't happen), reset
176                            app.current_page = Pages::Home;
177                        }
178                    }
179                }
180            }
181            _ => fallback_key_handler(app, key_event),
182        },
183        // Success popup - displays success messages
184        Popups::Success => match key_event.code {
185            KeyCode::Esc | KeyCode::Enter | KeyCode::Char(' ') => {
186                app.current_popup = None;
187                app.error_message = None;
188                // Navigate back to an appropriate page based on current context
189                match app.current_page {
190                    Pages::Lichess => {
191                        // If we're on Lichess-related pages, go back to Lichess menu
192                        app.current_page = Pages::LichessMenu;
193                    }
194                    Pages::OngoingGames => {
195                        // If we're on Ongoing Games page, stay in Ongoing Games menu,
196                        // and after resign success, refetch the list of ongoing games
197                        app.fetch_ongoing_games();
198                    }
199                    Pages::Multiplayer | Pages::Bot => {
200                        // If we're on multiplayer or bot page, go back to home
201                        app.current_page = Pages::Home;
202                    }
203                    _ => {
204                        // For other pages, stay on current page or go to home
205                        // Only change if we're in a weird state
206                        if app.current_page == Pages::Solo && app.game.logic.opponent.is_some() {
207                            // If we're in solo but have an opponent (shouldn't happen), reset
208                            app.current_page = Pages::Home;
209                        }
210                    }
211                }
212            }
213            _ => fallback_key_handler(app, key_event),
214        },
215        Popups::EnterGameCode => match key_event.code {
216            KeyCode::Enter => {
217                // Submit the entered game code
218                app.game.ui.prompt.submit_message();
219                let game_code = app.game.ui.prompt.message.clone();
220
221                if !game_code.is_empty() {
222                    // Join the game with the entered code
223                    app.current_page = Pages::Lichess;
224                    app.join_lichess_game_by_code(game_code);
225                } else {
226                    // No code entered, return to menu
227                    app.current_popup = None;
228                    app.current_page = Pages::LichessMenu;
229                }
230            }
231            KeyCode::Char(to_insert) => app.game.ui.prompt.enter_char(to_insert),
232            KeyCode::Backspace => app.game.ui.prompt.delete_char(),
233            KeyCode::Left => app.game.ui.prompt.move_cursor_left(),
234            KeyCode::Right => app.game.ui.prompt.move_cursor_right(),
235            KeyCode::Esc => {
236                // Cancel game code entry and return to Lichess menu
237                app.current_popup = None;
238                app.current_page = Pages::LichessMenu;
239            }
240            _ => fallback_key_handler(app, key_event),
241        },
242        Popups::EnterLichessToken => match key_event.code {
243            KeyCode::Enter => {
244                // Submit the entered token
245                app.game.ui.prompt.submit_message();
246                let token = app.game.ui.prompt.message.clone().trim().to_string();
247
248                if !token.is_empty() {
249                    // Save and validate the token
250                    app.save_and_validate_lichess_token(token);
251                } else {
252                    // No token entered, return to previous page
253                    app.current_popup = None;
254                }
255            }
256            KeyCode::Char(to_insert) => app.game.ui.prompt.enter_char(to_insert),
257            KeyCode::Backspace => app.game.ui.prompt.delete_char(),
258            KeyCode::Left => app.game.ui.prompt.move_cursor_left(),
259            KeyCode::Right => app.game.ui.prompt.move_cursor_right(),
260            KeyCode::Esc => {
261                // Cancel token entry
262                app.current_popup = None;
263            }
264            _ => fallback_key_handler(app, key_event),
265        },
266        Popups::SeekingLichessGame => match key_event.code {
267            KeyCode::Esc => {
268                if let Some(token) = &app.lichess_cancellation_token {
269                    token.store(true, std::sync::atomic::Ordering::Relaxed);
270                }
271                app.lichess_seek_receiver = None; // Cancel the receiver (thread continues but result ignored)
272                app.lichess_cancellation_token = None;
273                app.current_popup = None;
274                app.current_page = Pages::Home; // Go back to home
275            }
276            _ => fallback_key_handler(app, key_event),
277        },
278        Popups::ResignConfirmation => match key_event.code {
279            KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter => {
280                app.confirm_resign_game();
281                // fetch_ongoing_games() is already called in confirm_resign_game()
282            }
283            KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
284                app.current_popup = None;
285            }
286            _ => fallback_key_handler(app, key_event),
287        },
288        Popups::MoveInputSelection => match key_event.code {
289            KeyCode::Enter => {
290                // Submit the entered move
291                app.game.ui.prompt.submit_message();
292                let mut player_move = app.game.ui.prompt.message.clone().trim().to_string();
293
294                // normalize the input so if some letters are lower case, make it upper case, not doing so means invalid SAN because lower case denotes pawn only
295                player_move = normalize_lowercase_to_san(&player_move);
296
297                if player_move.is_empty() {
298                    app.current_popup = None;
299                    return;
300                }
301
302                let san = match San::from_ascii(player_move.as_bytes()) {
303                    Ok(san) => san,
304                    Err(_) => {
305                        app.current_popup = None;
306                        return;
307                    }
308                };
309
310                let position = app.game.logic.game_board.position_ref().clone();
311
312                let chess_move = match san.to_move(&position) {
313                    Ok(chess_move) => chess_move,
314                    Err(_) => {
315                        app.current_popup = None;
316                        return;
317                    }
318                };
319
320                let from = match chess_move.from() {
321                    Some(from) => from,
322                    None => {
323                        app.current_popup = None;
324                        return;
325                    }
326                };
327
328                let to = chess_move.to();
329                let promotion = chess_move.promotion();
330
331                app.game.apply_player_move(from, to, promotion);
332                app.current_popup = None;
333            }
334            KeyCode::Char(to_insert) => app.game.ui.prompt.enter_char(to_insert),
335            KeyCode::Backspace => app.game.ui.prompt.delete_char(),
336            KeyCode::Left => app.game.ui.prompt.move_cursor_left(),
337            KeyCode::Right => app.game.ui.prompt.move_cursor_right(),
338            KeyCode::Esc => {
339                app.current_popup = None;
340            }
341            _ => fallback_key_handler(app, key_event),
342        },
343    };
344}
345
346/// Routes keyboard input to the appropriate page handler based on current page.
347fn handle_page_input(app: &mut App, key_event: KeyEvent) {
348    match &app.current_page {
349        Pages::Home => handle_home_page_events(app, key_event),
350        Pages::Solo => handle_solo_page_events(app, key_event),
351        Pages::Multiplayer => handle_multiplayer_page_events(app, key_event),
352        Pages::Lichess => handle_multiplayer_page_events(app, key_event),
353        Pages::LichessMenu => handle_lichess_menu_page_events(app, key_event),
354        Pages::GameModeMenu => handle_game_mode_menu_page_events(app, key_event),
355        Pages::OngoingGames => handle_ongoing_games_page_events(app, key_event),
356        Pages::Bot => handle_bot_page_events(app, key_event),
357        Pages::Credit => handle_credit_page_events(app, key_event),
358    }
359}
360
361/// Handles keyboard input on the home/menu page.
362/// Supports navigation through menu items and selection.
363fn handle_home_page_events(app: &mut App, key_event: KeyEvent) {
364    // Number of menu items depends on whether sound feature is enabled
365    const MENU_ITEMS: u8 = {
366        #[cfg(feature = "sound")]
367        {
368            6 // Play Game, Lichess, Skin, Sound, Help, About
369        }
370        #[cfg(not(feature = "sound"))]
371        {
372            5 // Play Game, Lichess, Skin, Help, About
373        }
374    };
375
376    match key_event.code {
377        KeyCode::Up | KeyCode::Char('k') => app.menu_cursor_up(MENU_ITEMS),
378        KeyCode::Down | KeyCode::Char('j') => app.menu_cursor_down(MENU_ITEMS),
379        // If on skin selection menu item (index 2), use left/right to cycle skins
380        KeyCode::Left | KeyCode::Char('h') if app.menu_cursor == 2 => {
381            app.cycle_skin_backward();
382            app.update_config();
383        }
384        KeyCode::Right | KeyCode::Char('l') if app.menu_cursor == 2 => {
385            app.cycle_skin();
386            app.update_config();
387        }
388        KeyCode::Char(' ') | KeyCode::Enter => app.menu_select(),
389        KeyCode::Char('?') => app.toggle_help_popup(),
390        _ => fallback_key_handler(app, key_event),
391    }
392}
393
394/// Handles keyboard input during solo (two-player) game mode.
395fn handle_solo_page_events(app: &mut App, key_event: KeyEvent) {
396    match key_event.code {
397        KeyCode::Char('r') => app.restart(), // Restart current game
398        KeyCode::Char('b') => {
399            // Return to home menu - reset all game state
400            app.reset_home();
401        }
402        KeyCode::Char('n' | 'N') => {
403            // In puzzle mode, 'n' is used for new puzzle (handled in popup)
404            // Otherwise, navigate to next position in history
405            if app.puzzle_game.is_none()
406                && app.game.logic.game_state != GameState::Checkmate
407                && app.game.logic.game_state != GameState::Draw
408            {
409                app.navigate_history_next();
410            }
411        }
412        KeyCode::Char('p' | 'P') => {
413            // Navigate to previous position in history (only if game hasn't ended and not in puzzle mode)
414            if app.puzzle_game.is_none()
415                && app.game.logic.game_state != GameState::Checkmate
416                && app.game.logic.game_state != GameState::Draw
417            {
418                app.navigate_history_previous();
419            }
420        }
421        KeyCode::Char('t' | 'T') if app.puzzle_game.is_some() && app.current_popup.is_none() => {
422            // Show hint in puzzle mode (only when no popup is active)
423            app.show_puzzle_hint();
424        }
425        _ => chess_inputs(app, key_event), // Delegate chess-specific inputs
426    }
427}
428
429/// Handles chess-specific keyboard inputs (cursor movement, piece selection, etc.).
430///
431/// This function processes inputs that are common across all game modes (solo, bot, multiplayer).
432/// The behavior varies based on the current game state (Playing, Promotion, Checkmate, Draw).
433fn chess_inputs(app: &mut App, key_event: KeyEvent) {
434    let is_playing = app.game.logic.game_state == GameState::Playing;
435
436    match key_event.code {
437        // Vertical cursor movement (only during active play)
438        KeyCode::Up | KeyCode::Char('k') if is_playing => app.go_up_in_game(),
439        KeyCode::Down | KeyCode::Char('j') if is_playing => app.go_down_in_game(),
440
441        // Horizontal cursor movement - behavior depends on game state
442        KeyCode::Right | KeyCode::Char('l') => match app.game.logic.game_state {
443            GameState::Promotion => {
444                // Always allow promotion cursor movement, regardless of turn or page
445                app.game.ui.cursor_right_promotion();
446            }
447            GameState::Playing => {
448                // In Lichess mode, only allow board cursor movement if it's our turn
449                if app.current_page == Pages::Lichess {
450                    if let Some(my_color) = app.selected_color {
451                        if app.game.logic.player_turn == my_color {
452                            app.go_right_in_game();
453                        }
454                    }
455                } else {
456                    app.go_right_in_game();
457                }
458            }
459            _ => (),
460        },
461        KeyCode::Left | KeyCode::Char('h') => match app.game.logic.game_state {
462            GameState::Promotion => {
463                // Always allow promotion cursor movement, regardless of turn or page
464                app.game.ui.cursor_left_promotion();
465            }
466            GameState::Playing => {
467                // In Lichess mode, only allow board cursor movement if it's our turn
468                if app.current_page == Pages::Lichess {
469                    if let Some(my_color) = app.selected_color {
470                        if app.game.logic.player_turn == my_color {
471                            app.go_left_in_game();
472                        }
473                    }
474                } else {
475                    app.go_left_in_game();
476                }
477            }
478            GameState::Checkmate | GameState::Draw => {
479                // Toggle end screen visibility when game is over
480                // If popup is shown, hide it; if hidden and dismissed, show it again
481                if app.current_popup == Some(Popups::EndScreen) {
482                    // Hide the end screen
483                    app.current_popup = None;
484                    app.end_screen_dismissed = true;
485                } else if app.end_screen_dismissed {
486                    // Show the end screen again if it was dismissed (toggle back)
487                    app.end_screen_dismissed = false;
488                    app.show_end_screen();
489                } else {
490                    // Show the end screen if it hasn't been shown yet
491                    app.show_end_screen();
492                }
493            }
494        },
495        // Select/move piece or confirm action
496        KeyCode::Char(' ') | KeyCode::Enter => {
497            // In Lichess mode, only allow input if it's our turn
498            app.process_cell_click();
499        }
500        KeyCode::Char('?') => app.toggle_help_popup(), // Toggle help popup
501        KeyCode::Char('s' | 'S') => {
502            app.cycle_skin(); // Cycle through available skins
503            app.update_config();
504        }
505        KeyCode::Char('m') => {
506            app.game.ui.prompt.reset();
507            app.current_popup = Some(Popups::MoveInputSelection)
508        }
509        KeyCode::Esc => app.game.ui.unselect_cell(), // Deselect piece
510        _ => fallback_key_handler(app, key_event),
511    }
512}
513
514/// Handles keyboard input during multiplayer game mode.
515/// Similar to solo mode but includes cleanup of network connections.
516fn handle_multiplayer_page_events(app: &mut App, key_event: KeyEvent) {
517    match key_event.code {
518        KeyCode::Char('b') => {
519            // Return to home menu - disconnect from opponent and reset state
520            app.reset_home();
521        }
522
523        _ => chess_inputs(app, key_event), // Delegate chess-specific inputs
524    }
525}
526
527/// Handles keyboard input when playing against a bot.
528/// Includes restart functionality and bot state cleanup.
529fn handle_bot_page_events(app: &mut App, key_event: KeyEvent) {
530    match key_event.code {
531        KeyCode::Char('r') => app.restart(), // Restart current game
532        KeyCode::Char('b') => {
533            // Return to home menu - clean up bot and reset state
534            app.reset_home();
535        }
536        _ => chess_inputs(app, key_event), // Delegate chess-specific inputs
537    }
538}
539
540/// Handles keyboard input on the credits page.
541fn handle_credit_page_events(app: &mut App, key_event: KeyEvent) {
542    match key_event.code {
543        KeyCode::Char(' ') | KeyCode::Esc | KeyCode::Enter => app.toggle_credit_popup(),
544        _ => fallback_key_handler(app, key_event),
545    }
546}
547
548/// Fallback handler for keys that aren't handled by specific page/popup handlers.
549/// Provides global shortcuts like quit that work from anywhere.
550fn fallback_key_handler(app: &mut App, key_event: KeyEvent) {
551    match key_event.code {
552        KeyCode::Char('q') => app.quit(), // Quit application
553        KeyCode::Char('c' | 'C') if key_event.modifiers == KeyModifiers::CONTROL => app.quit(), // Ctrl+C to quit
554        KeyCode::Char('s') => app.cycle_skin(), // Cycle through available skins
555        _ => (),                                // Ignore other keys
556    }
557}
558
559/// Handles mouse click events for piece selection and movement.
560///
561/// Mouse input is only active during game pages (Solo, Bot, Multiplayer).
562/// Handles both board clicks and promotion selection clicks.
563pub fn handle_mouse_events(mouse_event: MouseEvent, app: &mut App) -> AppResult<()> {
564    // Handle clicks on GameModeMenu page for documentation links
565    if app.current_page == Pages::GameModeMenu {
566        // Documentation links are opened via keyboard shortcut ('d')
567        return Ok(());
568    }
569
570    // Mouse control only implemented for game pages, not home or credits
571    if app.current_page == Pages::Home || app.current_page == Pages::Credit {
572        return Ok(());
573    }
574
575    // Only process left mouse button clicks
576    if mouse_event.kind == MouseEventKind::Down(MouseButton::Left) {
577        // Ignore clicks when game has ended
578        if app.game.logic.game_state == GameState::Checkmate
579            || app.game.logic.game_state == GameState::Draw
580        {
581            return Ok(());
582        }
583        // Ignore clicks when a popup is active
584        if app.current_popup.is_some() {
585            return Ok(());
586        }
587
588        // Handle promotion piece selection via mouse
589        // Note: Promotion state should always allow input, even if turn has switched
590        // because the player needs to select the promotion piece after making the move
591        if app.game.logic.game_state == GameState::Promotion {
592            // Calculate which promotion option was clicked (0-3 for Queen, Rook, Bishop, Knight)
593            let x = (mouse_event.column - app.game.ui.top_x) / app.game.ui.width;
594            let y = (mouse_event.row - app.game.ui.top_y) / app.game.ui.height;
595            if x > 3 || y > 0 {
596                return Ok(()); // Click outside promotion area
597            }
598            app.game.ui.promotion_cursor = x as i8;
599
600            // Track if the move was correct (for puzzle mode)
601            let mut move_was_correct = true;
602
603            // If we have a pending promotion move, validate it now with the selected promotion piece
604            if let Some((from, to)) = app.pending_promotion_move.take() {
605                // Get the promotion piece from the cursor
606                let promotion_char = match app.game.ui.promotion_cursor {
607                    0 => 'q', // Queen
608                    1 => 'r', // Rook
609                    2 => 'b', // Bishop
610                    3 => 'n', // Knight
611                    _ => 'q', // Default to queen
612                };
613
614                // Construct full UCI move with promotion piece
615                let move_uci = format!("{}{}{}", from, to, promotion_char);
616
617                // Validate the puzzle move with the complete UCI
618                if app.puzzle_game.is_some() {
619                    if let Some(mut puzzle_game) = app.puzzle_game.take() {
620                        let (is_correct, message) = puzzle_game.validate_move(
621                            move_uci,
622                            &mut app.game,
623                            app.lichess_token.clone(),
624                        );
625
626                        move_was_correct = is_correct;
627                        app.puzzle_game = Some(puzzle_game);
628
629                        if let Some(msg) = message {
630                            if is_correct {
631                                app.error_message = Some(msg);
632                                app.current_popup = Some(Popups::PuzzleEndScreen);
633                            } else {
634                                app.error_message = Some(msg);
635                                app.current_popup = Some(Popups::Error);
636                            }
637                        }
638                    }
639                }
640            }
641
642            // Only handle promotion if the move was correct (or not in puzzle mode)
643            // If incorrect, reset_last_move already removed the move and reset the state
644            if move_was_correct || app.puzzle_game.is_none() {
645                // Don't flip board in puzzle mode
646                let should_flip = app.puzzle_game.is_none();
647                app.game.handle_promotion(should_flip);
648            } else {
649                // Move was incorrect in puzzle mode - ensure game state is reset
650                // reset_last_move should have already handled this, but make sure
651                if app.game.logic.game_state == GameState::Promotion {
652                    app.game.logic.game_state = GameState::Playing;
653                }
654            }
655            // Notify opponent in multiplayer games
656            if app.game.logic.opponent.is_some() {
657                app.game.logic.handle_multiplayer_promotion();
658            }
659            return Ok(());
660        }
661
662        // In Lichess mode, only allow input if it's our turn (but not for promotion, handled above)
663        if app.current_page == Pages::Lichess {
664            if let Some(my_color) = app.selected_color {
665                if app.game.logic.player_turn != my_color {
666                    return Ok(());
667                }
668            }
669        }
670
671        // Validate click is within board boundaries
672        if mouse_event.column < app.game.ui.top_x || mouse_event.row < app.game.ui.top_y {
673            return Ok(());
674        }
675        if app.game.ui.width == 0 || app.game.ui.height == 0 {
676            return Ok(());
677        }
678
679        // Calculate which board square was clicked (0-7 for both x and y)
680        let x = (mouse_event.column - app.game.ui.top_x) / app.game.ui.width;
681        let y = (mouse_event.row - app.game.ui.top_y) / app.game.ui.height;
682        if x > 7 || y > 7 {
683            return Ok(()); // Click outside board
684        }
685
686        // Mark that mouse was used (affects keyboard cursor positioning)
687        app.game.ui.mouse_used = true;
688        let coords: Coord = Coord::new(y as u8, x as u8);
689
690        // Convert coordinates to board square, handling board flip if needed
691        let square = match coords.try_to_square() {
692            Some(s) => s,
693            None => return Ok(()), // Invalid coordinates, ignore click
694        };
695
696        // Get piece color at clicked square (accounting for board flip)
697        let piece_color =
698            app.game
699                .logic
700                .game_board
701                .get_piece_color_at_square(&flip_square_if_needed(
702                    square,
703                    app.game.logic.game_board.is_flipped,
704                ));
705
706        // Handle click on empty square
707        if piece_color.is_none() {
708            // If no piece was previously selected, ignore the click
709            if app.game.ui.selected_square.is_none() {
710                return Ok(());
711            } else {
712                // Piece was selected - try to execute move to empty square
713                app.try_mouse_move(square, coords);
714            }
715        }
716        // Handle click on square with a piece
717        else if piece_color == Some(app.game.logic.player_turn) {
718            // Clicked on own piece
719            // First check if we have a piece selected and this square is a valid move destination
720            if let Some(selected_square) = app.game.ui.selected_square {
721                // Check if this is a castling attempt: king selected, rook clicked
722                let actual_selected =
723                    flip_square_if_needed(selected_square, app.game.logic.game_board.is_flipped);
724                let actual_clicked =
725                    flip_square_if_needed(square, app.game.logic.game_board.is_flipped);
726
727                let selected_role = app
728                    .game
729                    .logic
730                    .game_board
731                    .get_role_at_square(&actual_selected);
732                let clicked_role = app
733                    .game
734                    .logic
735                    .game_board
736                    .get_role_at_square(&actual_clicked);
737
738                // Check if king is selected and rook is clicked
739                if selected_role == Some(Role::King) && clicked_role == Some(Role::Rook) {
740                    // Determine castling destination based on rook position
741                    let castling_dest = match actual_clicked {
742                        Square::H1 => Square::G1, // Kingside for white
743                        Square::A1 => Square::C1, // Queenside for white
744                        Square::H8 => Square::G8, // Kingside for black
745                        Square::A8 => Square::C8, // Queenside for black
746                        _ => {
747                            // Not a castling rook, try normal move
748                            if app.try_mouse_move(square, coords) {
749                                return Ok(());
750                            }
751                            app.game.ui.selected_square = Some(square);
752                            return Ok(());
753                        }
754                    };
755
756                    // Check if castling is legal by checking if destination is in authorized positions
757                    let authorized_positions = app
758                        .game
759                        .logic
760                        .game_board
761                        .get_authorized_positions(app.game.logic.player_turn, &actual_selected);
762
763                    if authorized_positions.contains(&castling_dest) {
764                        // Castling is legal, execute it
765                        let castling_coords = Coord::from_square(flip_square_if_needed(
766                            castling_dest,
767                            app.game.logic.game_board.is_flipped,
768                        ));
769                        if app.try_mouse_move(
770                            flip_square_if_needed(
771                                castling_dest,
772                                app.game.logic.game_board.is_flipped,
773                            ),
774                            castling_coords,
775                        ) {
776                            return Ok(());
777                        }
778                    }
779                }
780
781                // Try normal move first
782                if app.try_mouse_move(square, coords) {
783                    // Move was executed successfully
784                    return Ok(());
785                }
786            }
787            // Otherwise, select the clicked piece
788            app.game.ui.selected_square = Some(square);
789        } else {
790            // Clicked on opponent's piece - try to capture if valid
791            if app.game.ui.selected_square.is_some() {
792                app.try_mouse_move(square, coords);
793            }
794            // No piece selected and clicked opponent piece - ignore (try_execute_move handles this)
795        }
796    }
797    Ok(())
798}
799
800/// Handles keyboard input on the Lichess menu page.
801/// Supports navigation through menu items and selection.
802fn handle_lichess_menu_page_events(app: &mut App, key_event: KeyEvent) {
803    match key_event.code {
804        KeyCode::Up | KeyCode::Char('k') => app.menu_cursor_up(5), // 5 menu options
805        KeyCode::Down | KeyCode::Char('j') => app.menu_cursor_down(5),
806        KeyCode::Char(' ') | KeyCode::Enter => {
807            // Handle menu selection
808            match app.menu_cursor {
809                0 => {
810                    // Seek Game
811                    if app.lichess_token.is_none()
812                        || app
813                            .lichess_token
814                            .as_ref()
815                            .map(|t| t.is_empty())
816                            .unwrap_or(true)
817                    {
818                        // Open interactive token entry popup
819                        app.current_popup = Some(Popups::EnterLichessToken);
820                        app.game.ui.prompt.reset();
821                        app.game.ui.prompt.message = "Enter your Lichess API token:".to_string();
822                        return;
823                    }
824                    app.menu_cursor = 0;
825                    app.current_page = Pages::Lichess;
826                    app.create_lichess_opponent();
827                }
828                1 => {
829                    // Puzzle
830                    if app.lichess_token.is_none()
831                        || app
832                            .lichess_token
833                            .as_ref()
834                            .map(|t| t.is_empty())
835                            .unwrap_or(true)
836                    {
837                        // Open interactive token entry popup
838                        app.current_popup = Some(Popups::EnterLichessToken);
839                        app.game.ui.prompt.reset();
840                        app.game.ui.prompt.message = "Enter your Lichess API token:".to_string();
841                        return;
842                    }
843                    app.start_puzzle_mode();
844                }
845                2 => {
846                    // My Ongoing Games
847                    if app.lichess_token.is_none()
848                        || app
849                            .lichess_token
850                            .as_ref()
851                            .map(|t| t.is_empty())
852                            .unwrap_or(true)
853                    {
854                        // Open interactive token entry popup
855                        app.current_popup = Some(Popups::EnterLichessToken);
856                        app.game.ui.prompt.reset();
857                        app.game.ui.prompt.message = "Enter your Lichess API token:".to_string();
858                        return;
859                    }
860                    app.fetch_ongoing_games();
861                }
862                3 => {
863                    // Join by Code
864                    if app.lichess_token.is_none()
865                        || app
866                            .lichess_token
867                            .as_ref()
868                            .map(|t| t.is_empty())
869                            .unwrap_or(true)
870                    {
871                        // Open interactive token entry popup
872                        app.current_popup = Some(Popups::EnterLichessToken);
873                        app.game.ui.prompt.reset();
874                        app.game.ui.prompt.message = "Enter your Lichess API token:".to_string();
875                        return;
876                    }
877                    app.current_popup = Some(Popups::EnterGameCode);
878                    app.game.ui.prompt.reset();
879                }
880                4 => {
881                    // Disconnect
882                    app.disconnect_lichess();
883                }
884                _ => {}
885            }
886        }
887        KeyCode::Esc | KeyCode::Char('b') => {
888            // Return to home menu
889            app.menu_cursor = 0;
890            app.current_page = Pages::Home;
891        }
892        KeyCode::Char('?') => app.toggle_help_popup(),
893        _ => fallback_key_handler(app, key_event),
894    }
895}
896
897/// Handles keyboard input on the Game Mode menu page.
898/// Supports navigation through menu items, form fields, and selection.
899fn handle_game_mode_menu_page_events(app: &mut App, key_event: KeyEvent) {
900    // Ensure cursor is valid (0-2)
901    if app.menu_cursor > 2 {
902        app.menu_cursor = 0;
903    }
904
905    let game_mode = app.menu_cursor;
906
907    // If form is active, handle form navigation
908    if app.game_mode_form_active {
909        match key_event.code {
910            KeyCode::Esc => {
911                // Deactivate form and go back to menu
912                app.game_mode_form_active = false;
913                app.game_mode_form_cursor = 0;
914            }
915            KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
916                // Up/Down navigation disabled in form mode
917                // Use Left/Right to toggle options and Enter/Space to move to next field
918            }
919            KeyCode::Left | KeyCode::Char('h') => {
920                // Navigate left - go to first option (Host/White)
921                match game_mode {
922                    0 => {
923                        // Local: time control selection
924                        match app.game_mode_form_cursor {
925                            0 => {
926                                // Time control - previous option (0-6)
927                                if app.clock_form_cursor > 0 {
928                                    app.clock_form_cursor -= 1;
929                                }
930                            }
931                            1 => {
932                                // Custom time - decrease (only if Custom is selected)
933                                if app.clock_form_cursor
934                                    == crate::constants::TIME_CONTROL_CUSTOM_INDEX
935                                    && app.custom_time_minutes > 1
936                                {
937                                    app.custom_time_minutes -= 1;
938                                }
939                            }
940                            _ => {}
941                        }
942                    }
943                    1 => {
944                        // Multiplayer
945                        match app.game_mode_form_cursor {
946                            0 => {
947                                // Set to Host
948                                app.hosting = Some(true);
949                            }
950                            1 => {
951                                // Set to White (only if hosting)
952                                if app.hosting == Some(true) {
953                                    app.selected_color = Some(shakmaty::Color::White);
954                                }
955                            }
956                            _ => {}
957                        }
958                    }
959                    2 => {
960                        // Bot
961                        match app.game_mode_form_cursor {
962                            0 => {
963                                // Time control - previous option (0-6)
964                                if app.clock_form_cursor > 0 {
965                                    app.clock_form_cursor -= 1;
966                                }
967                            }
968                            1 => {
969                                // Custom time or Color
970                                if app.clock_form_cursor
971                                    == crate::constants::TIME_CONTROL_CUSTOM_INDEX
972                                {
973                                    // Custom time - decrease (only if Custom is selected)
974                                    if app.custom_time_minutes > 1 {
975                                        app.custom_time_minutes -= 1;
976                                    }
977                                } else {
978                                    // Color - set to White
979                                    app.selected_color = Some(shakmaty::Color::White);
980                                }
981                            }
982                            2 => {
983                                // Color (if Custom selected) or Bot depth
984                                if app.clock_form_cursor
985                                    == crate::constants::TIME_CONTROL_CUSTOM_INDEX
986                                {
987                                    // Color - set to White
988                                    app.selected_color = Some(shakmaty::Color::White);
989                                } else {
990                                    // Bot depth - decrease
991                                    if app.bot_depth > 1 {
992                                        app.bot_depth -= 1;
993                                        app.update_config();
994                                    }
995                                }
996                            }
997                            3 => {
998                                // Bot depth (if Custom selected) or Difficulty (no custom)
999                                if app.clock_form_cursor
1000                                    == crate::constants::TIME_CONTROL_CUSTOM_INDEX
1001                                {
1002                                    if app.bot_depth > 1 {
1003                                        app.bot_depth -= 1;
1004                                        app.update_config();
1005                                    }
1006                                } else {
1007                                    // Difficulty - previous: Off -> Magnus -> Hard -> Medium -> Easy -> Off
1008                                    match app.bot_difficulty {
1009                                        None => {
1010                                            app.bot_difficulty =
1011                                                Some((BOT_DIFFICULTY_COUNT - 1) as u8)
1012                                        }
1013                                        Some(0) => app.bot_difficulty = None,
1014                                        Some(i) => app.bot_difficulty = Some(i - 1),
1015                                    }
1016                                    app.update_config();
1017                                }
1018                            }
1019                            4 => {
1020                                // Difficulty - previous
1021                                match app.bot_difficulty {
1022                                    None => {
1023                                        app.bot_difficulty = Some((BOT_DIFFICULTY_COUNT - 1) as u8)
1024                                    }
1025                                    Some(0) => app.bot_difficulty = None,
1026                                    Some(i) => app.bot_difficulty = Some(i - 1),
1027                                }
1028                                app.update_config();
1029                            }
1030                            _ => {}
1031                        }
1032                    }
1033                    _ => {}
1034                }
1035            }
1036            KeyCode::Right | KeyCode::Char('l') => {
1037                // Navigate right - go to second option (Join/Black)
1038                match game_mode {
1039                    0 => {
1040                        // Local: time control selection
1041                        match app.game_mode_form_cursor {
1042                            0 => {
1043                                // Time control - next option (0-6: UltraBullet, Bullet, Blitz, Rapid, Classical, No clock, Custom)
1044                                if app.clock_form_cursor
1045                                    < crate::constants::TIME_CONTROL_CUSTOM_INDEX
1046                                {
1047                                    app.clock_form_cursor += 1;
1048                                }
1049                            }
1050                            1 => {
1051                                // Custom time - increase (only if Custom is selected)
1052                                if app.clock_form_cursor
1053                                    == crate::constants::TIME_CONTROL_CUSTOM_INDEX
1054                                    && app.custom_time_minutes < 120
1055                                {
1056                                    app.custom_time_minutes += 1;
1057                                }
1058                            }
1059                            _ => {}
1060                        }
1061                    }
1062                    1 => {
1063                        // Multiplayer
1064                        match app.game_mode_form_cursor {
1065                            0 => {
1066                                // Set to Join
1067                                app.hosting = Some(false);
1068                            }
1069                            1 => {
1070                                // Set to Black (only if hosting)
1071                                if app.hosting == Some(true) {
1072                                    app.selected_color = Some(shakmaty::Color::Black);
1073                                }
1074                            }
1075                            _ => {}
1076                        }
1077                    }
1078                    2 => {
1079                        // Bot
1080                        match app.game_mode_form_cursor {
1081                            0 => {
1082                                // Time control - next option (0-6: UltraBullet, Bullet, Blitz, Rapid, Classical, No clock, Custom)
1083                                if app.clock_form_cursor
1084                                    < crate::constants::TIME_CONTROL_CUSTOM_INDEX
1085                                {
1086                                    app.clock_form_cursor += 1;
1087                                }
1088                            }
1089                            1 => {
1090                                // Custom time or Color
1091                                if app.clock_form_cursor
1092                                    == crate::constants::TIME_CONTROL_CUSTOM_INDEX
1093                                {
1094                                    // Custom time - increase (only if Custom is selected)
1095                                    if app.custom_time_minutes < 120 {
1096                                        app.custom_time_minutes += 1;
1097                                    }
1098                                } else {
1099                                    // Color - set to Black
1100                                    app.selected_color = Some(shakmaty::Color::Black);
1101                                }
1102                            }
1103                            2 => {
1104                                // Color (if Custom selected) or Bot depth
1105                                if app.clock_form_cursor
1106                                    == crate::constants::TIME_CONTROL_CUSTOM_INDEX
1107                                {
1108                                    // Color - set to Black
1109                                    app.selected_color = Some(shakmaty::Color::Black);
1110                                } else {
1111                                    // Bot depth - increase
1112                                    if app.bot_depth < 20 {
1113                                        app.bot_depth += 1;
1114                                        app.update_config();
1115                                    }
1116                                }
1117                            }
1118                            3 => {
1119                                // Bot depth (if Custom selected) or Difficulty (no custom)
1120                                if app.clock_form_cursor
1121                                    == crate::constants::TIME_CONTROL_CUSTOM_INDEX
1122                                {
1123                                    if app.bot_depth < 20 {
1124                                        app.bot_depth += 1;
1125                                        app.update_config();
1126                                    }
1127                                } else {
1128                                    // Difficulty - next: Off -> Easy -> Medium -> Hard -> Magnus -> Off
1129                                    match app.bot_difficulty {
1130                                        None => app.bot_difficulty = Some(0),
1131                                        Some(i) if i + 1 >= BOT_DIFFICULTY_COUNT as u8 => {
1132                                            app.bot_difficulty = None
1133                                        }
1134                                        Some(i) => app.bot_difficulty = Some(i + 1),
1135                                    }
1136                                    app.update_config();
1137                                }
1138                            }
1139                            4 => {
1140                                // Difficulty - next
1141                                match app.bot_difficulty {
1142                                    None => app.bot_difficulty = Some(0),
1143                                    Some(i) if i + 1 >= BOT_DIFFICULTY_COUNT as u8 => {
1144                                        app.bot_difficulty = None
1145                                    }
1146                                    Some(i) => app.bot_difficulty = Some(i + 1),
1147                                }
1148                                app.update_config();
1149                            }
1150                            _ => {}
1151                        }
1152                    }
1153                    _ => {}
1154                }
1155            }
1156            KeyCode::Char(' ') | KeyCode::Enter => {
1157                // Confirm current field and move to next, or start game if all fields filled
1158                match game_mode {
1159                    0 => {
1160                        // Local game - handle form navigation or start game
1161                        match app.game_mode_form_cursor {
1162                            0 => {
1163                                // On time control field
1164                                if app.clock_form_cursor
1165                                    == crate::constants::TIME_CONTROL_CUSTOM_INDEX
1166                                {
1167                                    // Custom selected - move to custom time field
1168                                    app.game_mode_form_cursor = 1;
1169                                } else {
1170                                    // Other time control - start game directly
1171                                    if let Some(seconds) = app.get_time_control_seconds() {
1172                                        use crate::game_logic::clock::Clock;
1173                                        app.game.logic.clock = Some(Clock::new(seconds));
1174                                    }
1175                                    app.current_page = Pages::Solo;
1176                                    app.game_mode_selection = None;
1177                                    app.game_mode_form_cursor = 0;
1178                                    app.game_mode_form_active = false;
1179                                }
1180                            }
1181                            1 => {
1182                                // On custom time field - start game
1183                                if let Some(seconds) = app.get_time_control_seconds() {
1184                                    use crate::game_logic::clock::Clock;
1185                                    app.game.logic.clock = Some(Clock::new(seconds));
1186                                }
1187                                app.current_page = Pages::Solo;
1188                                app.game_mode_selection = None;
1189                                app.game_mode_form_cursor = 0;
1190                                app.game_mode_form_active = false;
1191                            }
1192                            _ => {}
1193                        }
1194                    }
1195                    1 => {
1196                        // Multiplayer - step by step
1197                        match app.game_mode_form_cursor {
1198                            0 => {
1199                                // On Host/Join field - select default (Host) if nothing selected, then move to next
1200                                if app.hosting.is_none() {
1201                                    app.hosting = Some(true); // Default to Host
1202                                }
1203                                if app.hosting == Some(true) {
1204                                    // Hosting: move to color selection
1205                                    app.game_mode_form_cursor = 1;
1206                                } else {
1207                                    // Joining: can start game immediately
1208                                    app.current_page = Pages::Multiplayer;
1209                                    app.game_mode_selection = None;
1210                                    app.game_mode_form_cursor = 0;
1211                                    app.game_mode_form_active = false;
1212                                }
1213                            }
1214                            1 => {
1215                                // On Color field - select default (White) if nothing selected, then start game
1216                                if app.selected_color.is_none() {
1217                                    app.selected_color = Some(shakmaty::Color::White);
1218                                    // Default to White
1219                                }
1220                                // Hosting: start game (color selected)
1221                                app.current_page = Pages::Multiplayer;
1222                                app.game_mode_selection = None;
1223                                app.game_mode_form_cursor = 0;
1224                                app.game_mode_form_active = false;
1225                            }
1226                            _ => {}
1227                        }
1228                    }
1229                    2 => {
1230                        // Bot - step by step
1231                        match app.game_mode_form_cursor {
1232                            0 => {
1233                                // On time control field
1234                                if app.clock_form_cursor
1235                                    == crate::constants::TIME_CONTROL_CUSTOM_INDEX
1236                                {
1237                                    // Custom selected - move to custom time field
1238                                    app.game_mode_form_cursor = 1;
1239                                } else {
1240                                    // Other time control - move to color field
1241                                    app.game_mode_form_cursor = 1;
1242                                }
1243                            }
1244                            1 => {
1245                                // On custom time field (if Custom selected) or color field
1246                                if app.clock_form_cursor
1247                                    == crate::constants::TIME_CONTROL_CUSTOM_INDEX
1248                                {
1249                                    // Custom time field - move to color field
1250                                    app.game_mode_form_cursor = 2;
1251                                } else {
1252                                    // Color field - select default (White) if nothing selected, then move to depth
1253                                    if app.selected_color.is_none() {
1254                                        app.selected_color = Some(shakmaty::Color::White);
1255                                        // Default to White
1256                                    }
1257                                    app.game_mode_form_cursor = 2;
1258                                }
1259                            }
1260                            2 => {
1261                                // On color field (if Custom selected) or depth field
1262                                if app.clock_form_cursor
1263                                    == crate::constants::TIME_CONTROL_CUSTOM_INDEX
1264                                {
1265                                    // Color field - select default (White) if nothing selected, then move to depth
1266                                    if app.selected_color.is_none() {
1267                                        app.selected_color = Some(shakmaty::Color::White);
1268                                        // Default to White
1269                                    }
1270                                    app.game_mode_form_cursor = 3;
1271                                } else {
1272                                    // Depth field - move to ELO field
1273                                    app.game_mode_form_cursor = 3;
1274                                }
1275                            }
1276                            3 => {
1277                                // On depth field (if Custom selected) - move to ELO; on ELO field (no custom) - start game
1278                                if app.clock_form_cursor
1279                                    == crate::constants::TIME_CONTROL_CUSTOM_INDEX
1280                                {
1281                                    // Depth field - move to ELO
1282                                    app.game_mode_form_cursor = 4;
1283                                } else {
1284                                    // ELO field - start game
1285                                    app.current_page = Pages::Bot;
1286                                    app.game_mode_selection = None;
1287                                    app.game_mode_form_cursor = 0;
1288                                    app.game_mode_form_active = false;
1289                                }
1290                            }
1291                            4 => {
1292                                // On ELO field - start game
1293                                app.current_page = Pages::Bot;
1294                                app.game_mode_selection = None;
1295                                app.game_mode_form_cursor = 0;
1296                                app.game_mode_form_active = false;
1297                            }
1298                            _ => {}
1299                        }
1300                    }
1301                    _ => {}
1302                }
1303            }
1304            _ => fallback_key_handler(app, key_event),
1305        }
1306    } else {
1307        // Menu navigation mode (form not active)
1308        match key_event.code {
1309            KeyCode::Up | KeyCode::Char('k') => {
1310                app.menu_cursor_up(3);
1311            }
1312            KeyCode::Down | KeyCode::Char('j') => {
1313                app.menu_cursor_down(3);
1314            }
1315            KeyCode::Left | KeyCode::Char('h') => {
1316                // Change game mode selection
1317                if app.menu_cursor > 0 {
1318                    app.menu_cursor -= 1;
1319                }
1320            }
1321            KeyCode::Right | KeyCode::Char('l') => {
1322                // Change game mode selection
1323                if app.menu_cursor < 2 {
1324                    app.menu_cursor += 1;
1325                }
1326            }
1327            KeyCode::Char(' ') | KeyCode::Enter => {
1328                // Activate the form for all modes
1329                app.game_mode_form_active = true;
1330                app.game_mode_form_cursor = 0;
1331                app.game_mode_selection = Some(game_mode);
1332                // Reset form state
1333                if game_mode == 0 {
1334                    // Local game: reset clock time to default if needed
1335                    if app.clock_form_cursor > crate::constants::TIME_CONTROL_CUSTOM_INDEX {
1336                        app.clock_form_cursor = 3; // Default: Rapid
1337                    }
1338                } else {
1339                    // Activate the form for modes with configuration
1340                    app.game_mode_form_active = true;
1341                    app.game_mode_form_cursor = 0; // Start at first form field
1342                    app.game_mode_selection = Some(game_mode);
1343                    // Reset form state
1344                    app.hosting = None;
1345                    app.selected_color = None;
1346                }
1347            }
1348            KeyCode::Esc | KeyCode::Char('b') => {
1349                // Return to home menu
1350                app.menu_cursor = 0;
1351                app.game_mode_selection = None;
1352                app.game_mode_form_cursor = 0;
1353                app.game_mode_form_active = false;
1354                app.clock_form_cursor = 3; // Reset to default (Rapid)
1355                app.custom_time_minutes = 10; // Reset custom time
1356                app.current_page = Pages::Home;
1357            }
1358            KeyCode::Char('?') => app.toggle_help_popup(),
1359            _ => fallback_key_handler(app, key_event),
1360        }
1361    }
1362}
1363
1364fn handle_ongoing_games_page_events(app: &mut App, key_event: KeyEvent) {
1365    match key_event.code {
1366        KeyCode::Up | KeyCode::Char('k') => {
1367            if app.menu_cursor > 0 {
1368                app.menu_cursor -= 1;
1369            }
1370        }
1371        KeyCode::Down | KeyCode::Char('j') => {
1372            if (app.menu_cursor as usize) < app.ongoing_games.len().saturating_sub(1) {
1373                app.menu_cursor += 1;
1374            }
1375        }
1376        KeyCode::Enter | KeyCode::Char(' ') => {
1377            app.select_ongoing_game();
1378        }
1379        KeyCode::Char('r') | KeyCode::Char('R') => {
1380            // Resign game
1381            app.show_resign_confirmation();
1382        }
1383        KeyCode::Esc | KeyCode::Char('b') => {
1384            app.menu_cursor = 0;
1385            app.current_page = Pages::LichessMenu;
1386        }
1387        KeyCode::Char('?') => app.toggle_help_popup(),
1388        _ => fallback_key_handler(app, key_event),
1389    }
1390}