main.rs (6917B)
1 mod board; 2 3 use board::{Board, GRID_SIZE}; 4 use crossterm::{ 5 event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, 6 execute, 7 terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, 8 }; 9 use ratatui::{ 10 Frame, Terminal, 11 backend::{Backend, CrosstermBackend}, 12 layout::{Constraint, Direction, Layout, Rect}, 13 style::{Color, Modifier, Style}, 14 text::{Line, Span}, 15 widgets::{Block, Borders, Paragraph}, 16 }; 17 use std::io; 18 19 struct App { 20 board: Board, 21 cursor: (usize, usize), // (row, col) 22 } 23 24 impl App { 25 fn new() -> Self { 26 let mut board = Board::new(); 27 board.generate(); 28 Self { 29 board, 30 cursor: (0, 0), 31 } 32 } 33 34 fn move_cursor(&mut self, row_delta: isize, col_delta: isize) { 35 if self.board.is_solved() { 36 return; 37 } 38 let new_row = 39 (self.cursor.0 as isize + row_delta).clamp(0, (GRID_SIZE - 1) as isize) as usize; 40 let new_col = 41 (self.cursor.1 as isize + col_delta).clamp(0, (GRID_SIZE - 1) as isize) as usize; 42 self.cursor = (new_row, new_col); 43 } 44 45 fn set_cell_value(&mut self, val: Option<u8>) { 46 if self.board.is_solved() { 47 return; 48 } 49 self.board.set_value(self.cursor.0, self.cursor.1, val); 50 } 51 } 52 53 fn main() -> Result<(), Box<dyn std::error::Error>> { 54 enable_raw_mode()?; 55 let mut stdout = io::stdout(); 56 execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; 57 let backend = CrosstermBackend::new(stdout); 58 let mut terminal = Terminal::new(backend)?; 59 60 let mut app = App::new(); 61 let res = run_app(&mut terminal, &mut app); 62 63 disable_raw_mode()?; 64 execute!( 65 terminal.backend_mut(), 66 LeaveAlternateScreen, 67 DisableMouseCapture 68 )?; 69 terminal.show_cursor()?; 70 71 if let Err(err) = res { 72 eprintln!("{:?}", err) 73 } 74 75 Ok(()) 76 } 77 78 fn run_app<B: Backend>( 79 terminal: &mut Terminal<B>, 80 app: &mut App, 81 ) -> Result<(), Box<dyn std::error::Error>> 82 where 83 B::Error: std::error::Error + 'static, 84 { 85 loop { 86 terminal.draw(|f| ui(f, app))?; 87 88 if let Event::Key(key) = event::read()? { 89 match key.code { 90 KeyCode::Char('q') => return Ok(()), 91 KeyCode::Char('n') if app.board.is_solved() => { 92 app.board.generate(); 93 app.cursor = (0, 0); 94 } 95 KeyCode::Char('s') => { 96 app.board.solve(); 97 } 98 KeyCode::Char('h') | KeyCode::Left => app.move_cursor(0, -1), 99 KeyCode::Char('j') | KeyCode::Down => app.move_cursor(1, 0), 100 KeyCode::Char('k') | KeyCode::Up => app.move_cursor(-1, 0), 101 KeyCode::Char('l') | KeyCode::Right => app.move_cursor(0, 1), 102 KeyCode::Char(c) if c.is_digit(10) => { 103 let val = c.to_digit(10).unwrap() as u8; 104 if val == 0 { 105 app.set_cell_value(None); 106 } else { 107 app.set_cell_value(Some(val)); 108 } 109 } 110 KeyCode::Char('x') | KeyCode::Backspace => app.set_cell_value(None), 111 _ => {} 112 } 113 } 114 } 115 } 116 117 fn ui(f: &mut Frame, app: &App) { 118 let chunks = Layout::default() 119 .direction(Direction::Vertical) 120 .constraints([Constraint::Min(13), Constraint::Length(3)].as_ref()) 121 .split(f.area()); 122 123 draw_board(f, chunks[0], app); 124 draw_help(f, chunks[1], app.board.is_solved()); 125 } 126 127 fn draw_board(f: &mut Frame, area: Rect, app: &App) { 128 let board_width = 9 * 2 + 2; 129 let board_height = 9 + 2; 130 131 let board_area = Rect::new( 132 area.x + (area.width.saturating_sub(board_width as u16)) / 2, 133 area.y + (area.height.saturating_sub(board_height as u16)) / 2, 134 board_width as u16, 135 board_height as u16, 136 ); 137 138 let block = Block::default().borders(Borders::ALL).title(" Sudoku "); 139 f.render_widget(block, board_area); 140 141 let inner = board_area.inner(ratatui::layout::Margin { 142 vertical: 1, 143 horizontal: 1, 144 }); 145 146 for r in 0..GRID_SIZE { 147 for c in 0..GRID_SIZE { 148 let cell = app.board.cells[r][c]; 149 let is_cursor = (r, c) == app.cursor; 150 let is_conflict = app.board.is_conflict(r, c); 151 152 let mut style = Style::default(); 153 if cell.is_fixed { 154 style = style.add_modifier(Modifier::BOLD); 155 } else { 156 style = style.fg(Color::Cyan); 157 } 158 159 if is_conflict { 160 style = style.bg(Color::Red).fg(Color::White); 161 } 162 163 if is_cursor { 164 style = style.bg(Color::White).fg(Color::Black); 165 } 166 167 let text = match cell.value { 168 Some(v) => v.to_string(), 169 None => " ".to_string(), 170 }; 171 172 let x_offset = c as u16 * 2; 173 174 f.render_widget( 175 Paragraph::new(text).style(style), 176 Rect::new(inner.x + x_offset, inner.y + r as u16, 1, 1), 177 ); 178 179 if c % 3 == 2 && c < 8 { 180 f.render_widget( 181 Paragraph::new("│").style(Style::default().fg(Color::DarkGray)), 182 Rect::new(inner.x + x_offset + 1, inner.y + r as u16, 1, 1), 183 ); 184 } 185 } 186 } 187 } 188 189 fn draw_help(f: &mut Frame, area: Rect, solved: bool) { 190 let help_text = if solved { 191 vec![Line::from(vec![ 192 Span::styled( 193 "CONGRATULATIONS! ", 194 Style::default() 195 .fg(Color::Yellow) 196 .add_modifier(Modifier::BOLD), 197 ), 198 Span::raw("Press "), 199 Span::styled("n", Style::default().add_modifier(Modifier::BOLD)), 200 Span::raw(" for new game, "), 201 Span::styled("q", Style::default().add_modifier(Modifier::BOLD)), 202 Span::raw(": quit"), 203 ])] 204 } else { 205 vec![Line::from(vec![ 206 Span::styled("hjkl/Arrows", Style::default().add_modifier(Modifier::BOLD)), 207 Span::raw(": move, "), 208 Span::styled("1-9", Style::default().add_modifier(Modifier::BOLD)), 209 Span::raw(": input, "), 210 Span::styled("x/0/BS", Style::default().add_modifier(Modifier::BOLD)), 211 Span::raw(": delete, "), 212 Span::styled("s", Style::default().add_modifier(Modifier::BOLD)), 213 Span::raw(": solve, "), 214 Span::styled("q", Style::default().add_modifier(Modifier::BOLD)), 215 Span::raw(": quit"), 216 ])] 217 }; 218 let help = 219 Paragraph::new(help_text).block(Block::default().borders(Borders::ALL).title(" Help ")); 220 f.render_widget(help, area); 221 }