├── .gitignore ├── crossword ├── puzzles │ ├── version-1.2-puzzle.puz │ └── README.md ├── Cargo.toml ├── README.md └── src │ ├── checksum.rs │ ├── main.rs │ ├── puz.rs │ └── lib.rs ├── Cargo.toml ├── crosstui ├── README.md ├── Cargo.toml └── src │ └── main.rs ├── rustfmt.toml ├── README.md ├── LICENSE └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /crossword/puzzles/version-1.2-puzzle.puz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatrixFrog/crossword/HEAD/crossword/puzzles/version-1.2-puzzle.puz -------------------------------------------------------------------------------- /crossword/puzzles/README.md: -------------------------------------------------------------------------------- 1 | # Example puzzles for testing 2 | 3 | Borrowed from [Crosshare's repo](https://github.com/crosshare-org/crosshare/tree/master/app/__tests__/converter/puz). 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["crosstui", "crossword"] 4 | 5 | [workspace.package] 6 | version = "0.2.0" 7 | homepage = "https://github.com/MatrixFrog/crossword" 8 | repository = "https://github.com/MatrixFrog/crossword" 9 | edition = "2024" 10 | -------------------------------------------------------------------------------- /crossword/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crossword" 3 | license = "AGPL-3.0-only" 4 | description = "Library for crossword puzzles including .puz file parsing" 5 | readme = "README.md" 6 | version.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | edition.workspace = true 10 | 11 | [dependencies] 12 | encoding = "0.2" 13 | -------------------------------------------------------------------------------- /crosstui/README.md: -------------------------------------------------------------------------------- 1 | # Crosstui 2 | 3 | Play crossword puzzles in your terminal! 4 | 5 | First, find a `.puz` file. Many crossword sites allow you to download puzzles in this format, such as or . 6 | 7 | Then, run `crosstui`, passing the path to your `.puz` file as an argument: 8 | 9 | ```sh 10 | cargo install crosstui 11 | crosstui /path/to/some/crossword.puz 12 | ``` 13 | -------------------------------------------------------------------------------- /crosstui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crosstui" 3 | description = "Solve crossword puzzles in your terminal" 4 | license = "AGPL-3.0-only" 5 | version.workspace = true 6 | homepage.workspace = true 7 | repository.workspace = true 8 | edition.workspace = true 9 | 10 | [dependencies] 11 | clap = { version = "4.5.41", features = ["derive"] } 12 | crossterm = "0.29.0" 13 | crossword = { path = "../crossword", version = "0.2.0" } 14 | ratatui = "0.30.0-alpha" 15 | ratatui-macros = "0.7.0-alpha" 16 | -------------------------------------------------------------------------------- /crossword/README.md: -------------------------------------------------------------------------------- 1 | # Crossword 2 | 3 | A library for crossword puzzles! 4 | 5 | ## Playing crosswords 6 | 7 | This crate comes with no UI so if you want to actually play a crossword, try [crosstui](https://crates.io/crates/crosstui) which is backed by this crate. 8 | 9 | ## Writing your own crossword app 10 | 11 | If you want to use this crate to build your own crossword app, [`Puzzle`](https://docs.rs/crossword/latest/crossword/struct.Puzzle.html) is the main type you will use. Use `Puzzle::parse` to get a puzzle from a `.puz` file. See `crosstui` for an example. 12 | -------------------------------------------------------------------------------- /crossword/src/checksum.rs: -------------------------------------------------------------------------------- 1 | //! Checksum logic. See 2 | //! 3 | 4 | #[must_use] 5 | pub(crate) fn checksum_region(base: &[u8], input_checksum: u16) -> u16 { 6 | let mut checksum = input_checksum; 7 | for &byte in base { 8 | if checksum & 0x0001_u16 != 0 { 9 | checksum = (checksum >> 1) + 0x8000 10 | } else { 11 | checksum = checksum >> 1; 12 | } 13 | checksum = checksum.overflowing_add(byte as u16).0; 14 | } 15 | checksum 16 | } 17 | 18 | /// For metadata (title, author, copyright, or notes), we do nothing if the string is 19 | /// empty, but if it's not empty we include the \0 byte in the calculation. 20 | #[must_use] 21 | pub(crate) fn checksum_metadata_string(s: &[u8], input_checksum: u16) -> u16 { 22 | if s == b"\0" { 23 | return input_checksum; 24 | } 25 | 26 | checksum_region(s, input_checksum) 27 | } 28 | 29 | /// For clues, we do not include the trailing \0 byte. 30 | #[must_use] 31 | pub(crate) fn checksum_clue(s: &[u8], input_checksum: u16) -> u16 { 32 | checksum_region(&s[0..s.len() - 1], input_checksum) 33 | } 34 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # configuration for https://rust-lang.github.io/rustfmt/ 2 | 3 | # These represent a reasonably opinionated set of defaults for Rust code formatting. 4 | # Mostly rustfmt's defaults are used, but the following options generally improve the readability 5 | # of the code and reduce the number of lines in a file. 6 | 7 | ## Stable options 8 | ## These options are available in the stable version of Rust 9 | 10 | # convert Foo { x: x } to Foo { x } to avoid repetition 11 | use_field_init_shorthand = true 12 | 13 | 14 | ## unstable options 15 | ## These options require the nightly version of Rust in order to apply 16 | 17 | # 80 character limit on comments tends to be too narrow for prose 18 | comment_width = 100 19 | 20 | # format code the same way as the rest of the code to avoid inconsistencies 21 | format_code_in_doc_comments = true 22 | 23 | # apply formatting to declarative macros 24 | format_macro_matchers = true 25 | 26 | # grouping imports makes the order of imports deterministic 27 | group_imports = "StdExternalCrate" 28 | 29 | # this has a tendency to reduce the number of lines in a file and improves readability due to less 30 | # nesting 31 | imports_granularity = "Module" 32 | 33 | # don't use #[doc] when /// will suffice 34 | normalize_doc_attributes = true 35 | 36 | # necessary to force comments to be wrapped 37 | wrap_comments = true 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Crossword and Crosstui 2 | 3 | [![animated gif of crosstui in action](https://asciinema.org/a/7QP9xIRD7EDB6UluaoOSbbfSc.svg)](https://asciinema.org/a/7QP9xIRD7EDB6UluaoOSbbfSc) 4 | 5 | This is an experimental project for solving crosswords in the terminal. I built it mainly because I wanted to build something fun with [Ratatui], a library for TUIs (user interfaces that run in the terminal). 6 | 7 | If you like crosswords and running things in the terminal, please give it a try! 8 | 9 | ## To play a crossword 10 | 11 | First, find a `.puz` file. Many crossword sites allow you to download puzzles in this format, such as or . Or, use one of the examples in this repo, in `crossword/puzzles/`. 12 | 13 | Then, run the `crosstui` binary, passing the path to the `.puz` file as an argument: 14 | 15 | ```sh 16 | cargo run --release --bin=crosstui -- /path/to/your/crossword.puz 17 | ``` 18 | 19 | ## Code organization 20 | 21 | The code is separated into two crates. 22 | 23 | The `crossword` crate understands `.puz` files and how crossword puzzles work, but has no UI. Its primary type is the `Puzzle` struct, and it should be possible to build a separate crossword app using a different UI framework, by depending on `crossword` only. 24 | 25 | The `crosstui` crate is the interactive UI for displaying and solving puzzles, which uses the `crossword` crate to model the puzzle, and [Ratatui] for the UI. 26 | 27 | [Ratatui]: https://ratatui.rs/ 28 | -------------------------------------------------------------------------------- /crossword/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{env, fs}; 2 | 3 | use crossword::{ChecksumMismatch, Error, Puzzle}; 4 | 5 | fn parse_puzzle(path: &str) -> Result<(Puzzle, Vec), Error> { 6 | let data: Vec = fs::read(path)?; 7 | Puzzle::parse(data) 8 | } 9 | 10 | /// A simple CLI for testing .puz parsing 11 | fn main() -> Result<(), Error> { 12 | let args: Vec = env::args().collect(); 13 | 14 | let path = &args[1]; 15 | if fs::metadata(path)?.is_dir() { 16 | let mut success = 0; 17 | let mut failure = 0; 18 | 19 | for entry in fs::read_dir(path)? { 20 | let puz_path = entry.unwrap().path(); 21 | if let Some(p) = puz_path.to_str() { 22 | match parse_puzzle(p) { 23 | Ok((puzzle, checksum_mismatches)) => { 24 | if checksum_mismatches.is_empty() { 25 | println!("Parsed '{}' successfully from {}", puzzle.title(), p); 26 | } else { 27 | println!( 28 | "Parsed '{}' with checksum mismatches: {:?}", 29 | puzzle.title(), 30 | checksum_mismatches 31 | ); 32 | } 33 | success += 1; 34 | } 35 | Err(e) => { 36 | println!("Failed with {:?} from {}", e, p); 37 | failure += 1; 38 | } 39 | } 40 | } 41 | } 42 | dbg!(success, failure); 43 | } else { 44 | match parse_puzzle(path) { 45 | Ok((puz, checksum_mismatches)) => { 46 | if checksum_mismatches.is_empty() { 47 | println!("Parsed '{}' successfully from {}", puz.title(), path); 48 | dbg!(puz); 49 | } else { 50 | println!( 51 | "Parsed '{}' with checksum mismatches: {:?}", 52 | puz.title(), 53 | checksum_mismatches 54 | ); 55 | } 56 | } 57 | Err(e) => { 58 | println!("Failed with: {:?}", e); 59 | } 60 | } 61 | } 62 | 63 | Ok(()) 64 | } 65 | -------------------------------------------------------------------------------- /crosstui/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::path::{Path, PathBuf}; 3 | 4 | use clap::Parser; 5 | use clap::builder::Styles; 6 | use clap::builder::styling::AnsiColor; 7 | use crossterm::event::{self, KeyCode, KeyEvent}; 8 | use crossword::{Direction, Puzzle, Square, SquareStyle}; 9 | use ratatui::DefaultTerminal; 10 | use ratatui::buffer::Buffer; 11 | use ratatui::layout::{Constraint, Layout, Rect}; 12 | use ratatui::style::{Color, Style, Stylize}; 13 | use ratatui::text::Line; 14 | use ratatui::widgets::{Block, List, ListState, Padding, Paragraph, StatefulWidget, Widget, Wrap}; 15 | use ratatui_macros::{line, text}; 16 | 17 | const SQUARE_WIDTH: u16 = 7; 18 | const SQUARE_HEIGHT: u16 = 3; 19 | 20 | const HELP_STYLES: Styles = Styles::styled() 21 | .header(AnsiColor::Blue.on_default().bold()) 22 | .usage(AnsiColor::Blue.on_default().bold()) 23 | .literal(AnsiColor::White.on_default()) 24 | .placeholder(AnsiColor::Green.on_default()); 25 | 26 | #[derive(Parser, Debug)] 27 | #[command(version, about, author, styles = HELP_STYLES)] 28 | struct Cli { 29 | /// The path to the .puz file 30 | path: PathBuf, 31 | } 32 | 33 | fn main() -> io::Result<()> { 34 | let args = Cli::parse(); 35 | let puzzle = load_puzzle(&args.path); 36 | let app = App::new(puzzle); 37 | ratatui::run(|terminal| app.run(terminal)) 38 | } 39 | 40 | pub fn load_puzzle(path: &Path) -> Puzzle { 41 | let data: Vec = std::fs::read(path).unwrap_or_else(|err| { 42 | println!("{:?}", err); 43 | std::process::exit(1); 44 | }); 45 | let (puzzle, checksum_mismatches) = Puzzle::parse(data).unwrap_or_else(|e| { 46 | println!("Failed to parse .puz file: {:?}", e); 47 | std::process::exit(2); 48 | }); 49 | 50 | if !checksum_mismatches.is_empty() { 51 | println!( 52 | ".puz file parsing encountered checksum mismatches: {:?}", 53 | checksum_mismatches 54 | ); 55 | std::process::exit(3); 56 | } 57 | puzzle 58 | } 59 | 60 | #[derive(Debug)] 61 | pub struct App { 62 | puzzle: Puzzle, 63 | running: bool, 64 | } 65 | 66 | impl App { 67 | fn new(puzzle: Puzzle) -> Self { 68 | Self { 69 | puzzle, 70 | running: true, 71 | } 72 | } 73 | 74 | pub fn run(mut self, terminal: &mut DefaultTerminal) -> io::Result<()> { 75 | self.running = true; 76 | while self.running { 77 | terminal.draw(|frame| frame.render_widget(&self, frame.area()))?; 78 | self.handle_crossterm_events()?; 79 | } 80 | Ok(()) 81 | } 82 | 83 | /// Reads the crossterm events and updates the state of [`App`]. 84 | fn handle_crossterm_events(&mut self) -> io::Result<()> { 85 | if let Some(key) = event::read()?.as_key_press_event() { 86 | self.on_key_event(key); 87 | } 88 | Ok(()) 89 | } 90 | 91 | /// Handles the key events and updates the state of [`App`]. 92 | fn on_key_event(&mut self, key: KeyEvent) { 93 | match key.code { 94 | KeyCode::Esc => self.quit(), 95 | KeyCode::Up => self.puzzle.cursor_up(), 96 | KeyCode::Down => self.puzzle.cursor_down(), 97 | KeyCode::Left => self.puzzle.cursor_left(), 98 | KeyCode::Right => self.puzzle.cursor_right(), 99 | KeyCode::Backspace => { 100 | self.puzzle.erase_letter(); 101 | self.puzzle.backup_cursor(); 102 | } 103 | KeyCode::Delete => self.puzzle.erase_letter(), 104 | KeyCode::Tab => self.puzzle.advance_cursor_to_next_word(), 105 | KeyCode::Char(' ') => self.puzzle.swap_cursor_direction(), 106 | KeyCode::Char(letter) => { 107 | if letter.is_ascii_alphabetic() { 108 | self.puzzle.add_letter(letter); 109 | self.puzzle.move_cursor_to_next_empty_in_current_word(); 110 | } 111 | } 112 | _ => {} 113 | } 114 | } 115 | 116 | /// Set running to false to quit the application. 117 | fn quit(&mut self) { 118 | self.running = false; 119 | } 120 | } 121 | 122 | impl Widget for &App { 123 | fn render(self, area: Rect, buf: &mut Buffer) { 124 | let vertical = Layout::vertical([Constraint::Length(2), Constraint::Fill(1)]); 125 | let [title_area, main_area] = area.layout(&vertical); 126 | 127 | let title = text![ 128 | "", 129 | line!["Crosstui".light_blue(), ": ", self.puzzle.title()] 130 | .bold() 131 | .centered(), 132 | ]; 133 | title.render(title_area, buf); 134 | 135 | let horizontal = Layout::horizontal([Constraint::Length(45), Constraint::Percentage(100)]); 136 | let [left_area, puzzle_area] = main_area.layout(&horizontal); 137 | 138 | PuzzleGrid::new(&self.puzzle).render(puzzle_area, buf); 139 | 140 | let layout = Layout::vertical([ 141 | Constraint::Length(5), 142 | Constraint::Length(4), 143 | Constraint::Fill(1), 144 | Constraint::Fill(1), 145 | Constraint::Length(8), 146 | ]); 147 | let [ 148 | instructions_area, 149 | current_clue_area, 150 | across_clue_area, 151 | down_clue_area, 152 | metadata_area, 153 | ] = left_area.layout(&layout); 154 | 155 | let instructions = line![ 156 | "Instructions: ".bold(), 157 | "Use the arrow keys, space, and tab, to navigate the puzzle. Press escape to exit." 158 | .gray(), 159 | ]; 160 | Paragraph::new(instructions) 161 | .wrap(Wrap::default()) 162 | .render(instructions_area, buf); 163 | 164 | if self.puzzle.is_solved() { 165 | Paragraph::new("You solved it!") 166 | .block( 167 | Block::bordered() 168 | .title(Line::from(" Congratulations! ").centered()) 169 | .padding(Padding::uniform(2)), 170 | ) 171 | .render(current_clue_area, buf) 172 | } else { 173 | let (num, direction) = self.puzzle.current_clue_identifier(); 174 | Paragraph::new(line![ 175 | format!("{}{}", num, direction.to_char()).light_red(), 176 | ". ", 177 | self.puzzle.current_clue() 178 | ]) 179 | .wrap(Wrap::default()) 180 | .render(current_clue_area, buf); 181 | } 182 | 183 | ClueList::new(&self.puzzle, Direction::Across).render(across_clue_area, buf); 184 | ClueList::new(&self.puzzle, Direction::Down).render(down_clue_area, buf); 185 | 186 | let mut metadata: Vec = vec!["".into()]; 187 | let author = self.puzzle.author(); 188 | if !author.is_empty() { 189 | metadata.push("".into()); 190 | metadata.push(Line::from(vec!["Author: ".bold(), author.into()])) 191 | } 192 | let notes = self.puzzle.notes(); 193 | if !notes.is_empty() { 194 | metadata.push("".into()); 195 | metadata.push(notes.into()); 196 | } 197 | let copyright = self.puzzle.copyright(); 198 | if !copyright.is_empty() { 199 | metadata.push("".into()); 200 | metadata.push(copyright.into()); 201 | } 202 | 203 | Paragraph::new(metadata) 204 | .wrap(Wrap::default()) 205 | .render(metadata_area, buf); 206 | } 207 | } 208 | 209 | // A widget that renders the puzzle grid. 210 | struct PuzzleGrid<'a> { 211 | puzzle: &'a Puzzle, 212 | } 213 | 214 | impl<'a> PuzzleGrid<'a> { 215 | /// Creates a new `PuzzleGrid` widget. 216 | pub fn new(puzzle: &'a Puzzle) -> Self { 217 | Self { puzzle } 218 | } 219 | } 220 | 221 | impl Widget for PuzzleGrid<'_> { 222 | fn render(self, area: Rect, buf: &mut Buffer) { 223 | let grid = self.puzzle.grid(); 224 | let grid_area = area.centered( 225 | Constraint::Length(grid.width() as u16 * (2 + SQUARE_WIDTH)), 226 | Constraint::Length(grid.height() as u16 * (1 + SQUARE_HEIGHT)), 227 | ); 228 | 229 | for row in 0..grid.height() { 230 | for col in 0..grid.width() { 231 | let square = grid.get((row, col)); 232 | let style = to_ratatui_style(self.puzzle.square_style((row, col))); 233 | let square_area = Rect { 234 | x: grid_area.x + col as u16 * (SQUARE_WIDTH + 2), 235 | y: grid_area.y + row as u16 * (SQUARE_HEIGHT + 1), 236 | width: SQUARE_WIDTH, 237 | height: SQUARE_HEIGHT, 238 | }; 239 | render_square(square, style, square_area, buf); 240 | } 241 | } 242 | } 243 | } 244 | 245 | /// A widget that renders a list of clues 246 | struct ClueList<'a> { 247 | puzzle: &'a Puzzle, 248 | direction: Direction, 249 | } 250 | 251 | impl<'a> ClueList<'a> { 252 | pub fn new(puzzle: &'a Puzzle, direction: Direction) -> Self { 253 | Self { puzzle, direction } 254 | } 255 | } 256 | 257 | impl Widget for ClueList<'_> { 258 | fn render(self, area: Rect, buf: &mut Buffer) { 259 | let current_clue_identifier = self.puzzle.current_clue_identifier(); 260 | let cross_clue_identifier = self.puzzle.cross_clue_identifier(); 261 | 262 | let mut list_state = ListState::default(); 263 | let lines = self 264 | .puzzle 265 | .clues(self.direction) 266 | .into_iter() 267 | .enumerate() 268 | .map(|(index, (num, clue))| { 269 | if current_clue_identifier == (num, self.direction) 270 | || cross_clue_identifier.is_some_and(|cross_clue_identifier| { 271 | cross_clue_identifier == (num, self.direction) 272 | }) 273 | { 274 | list_state.select(Some(index)); 275 | }; 276 | line![num.to_string().light_red(), ". ", clue] 277 | }) 278 | .collect::>(); 279 | 280 | let highlight_style = if self.direction == self.puzzle.cursor_direction() { 281 | Style::default().black().bold().on_light_yellow() 282 | } else { 283 | Style::default().blue().bold().on_gray() 284 | }; 285 | 286 | let clue_list = List::new(lines).highlight_style(highlight_style).block( 287 | Block::bordered() 288 | .title(line![" ", self.direction.to_string(), " clues "].centered()) 289 | .padding(Padding { 290 | left: 2, 291 | right: 2, 292 | top: 1, 293 | bottom: 1, 294 | }), 295 | ); 296 | 297 | StatefulWidget::render(clue_list, area, buf, &mut list_state); 298 | } 299 | } 300 | 301 | fn to_ratatui_style(value: SquareStyle) -> Style { 302 | let bg = match value { 303 | SquareStyle::Standard => Color::White, 304 | SquareStyle::Cursor => Color::LightRed, 305 | SquareStyle::Word => Color::LightYellow, 306 | }; 307 | Style::new().bg(bg).black().bold() 308 | } 309 | 310 | fn render_square(square: Square, style: Style, area: Rect, buf: &mut Buffer) { 311 | match square { 312 | Square::Black => Block::new().on_black().render(area, buf), 313 | Square::Empty => Block::new().style(style).render(area, buf), 314 | Square::Letter(c) => { 315 | Paragraph::new(c.to_string()) 316 | .block(Block::new().style(style).padding(Padding::top(1))) 317 | .centered() 318 | .render(area, buf); 319 | } 320 | }; 321 | } 322 | -------------------------------------------------------------------------------- /crossword/src/puz.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fmt::{Debug, Display}; 3 | 4 | use encoding::DecoderTrap::Strict; 5 | use encoding::Encoding; 6 | use encoding::all::{ISO_8859_1, UTF_8}; 7 | 8 | use crate::Direction::{Across, Down}; 9 | use crate::{ClueIdentifier, checksum::*}; 10 | use crate::{Error, Grid, Pos}; 11 | 12 | /// A `Puz` is essentially only the data in a .puz file. For an interactively solvable 13 | /// puzzle, use `Puzzle` which includes a Cursor. 14 | #[derive(Debug)] 15 | pub(crate) struct Puz { 16 | pub(crate) solution: Grid, 17 | pub(crate) solve_state: Grid, 18 | pub(crate) title: String, 19 | pub(crate) author: String, 20 | pub(crate) copyright: String, 21 | pub(crate) notes: String, 22 | /// Mapping from grid positions to numbers. 23 | pub(crate) numbered_squares: HashMap, 24 | /// Mapping from [`ClueIdentifier``]s to clues. 25 | pub(crate) clues: HashMap, 26 | } 27 | 28 | impl Puz { 29 | /// Creates a Puz from the bytes of a `.puz` file. 30 | pub(crate) fn parse(data: Vec) -> Result<(Self, Vec), Error> { 31 | // There is no official spec for the puz file format but I'm following 32 | // 33 | // here and it seems to work well. 34 | 35 | let mut checksum_mismatches = vec![]; 36 | 37 | let cib_checksum_expected = checksum_region(&data[0x2C..0x34], 0); 38 | 39 | let mut scanner = Scanner::new(data); 40 | 41 | let overall_checksum = scanner.parse_short()?; 42 | scanner.take_exact(b"ACROSS&DOWN\0")?; 43 | 44 | let cib_checksum = scanner.parse_short()?; 45 | if cib_checksum != cib_checksum_expected { 46 | checksum_mismatches.push(ChecksumMismatch { 47 | checksum: Checksum::CIB, 48 | expected: cib_checksum_expected, 49 | actual: cib_checksum, 50 | }); 51 | } 52 | 53 | let masked_checksums = scanner.take_n_bytes(8)?; 54 | 55 | // Version string 56 | let _ = scanner.take_n_bytes(4)?; 57 | // Reserved 1C 58 | let _ = scanner.take_n_bytes(2)?; 59 | // Scrambled checksum 60 | let _ = scanner.take_n_bytes(2)?; 61 | 62 | // Nothing listed for these but it says the scrambled checksum ends at 0x1F and the 63 | // width should be at 0x2C so I guess we're just supposed to skip 0x20 through 0x2B? 64 | let _ = scanner.take_n_bytes(12)?; 65 | 66 | let width = *scanner.pop()? as usize; 67 | let height = *scanner.pop()? as usize; 68 | 69 | let num_clues = scanner.parse_short()?; 70 | 71 | // Unknown bitmask 72 | let _ = scanner.parse_short()?; 73 | 74 | let scrambled_tag = scanner.parse_short()?; 75 | if scrambled_tag != 0 { 76 | return Err(Error::ScrambledError); 77 | } 78 | 79 | let solution_bytes = scanner.take_n_bytes(width * height)?; 80 | let solution = Grid::parse(&solution_bytes, width, height); 81 | 82 | let solve_state_bytes = scanner.take_n_bytes(width * height)?; 83 | let solve_state = Grid::parse(&solve_state_bytes, width, height); 84 | 85 | let title = scanner.parse_nul_terminated_string()?; 86 | let author = scanner.parse_nul_terminated_string()?; 87 | let copyright = scanner.parse_nul_terminated_string()?; 88 | 89 | let mut clues = Vec::with_capacity(num_clues as usize); 90 | for _ in 0..num_clues { 91 | clues.push(scanner.parse_nul_terminated_string()?); 92 | } 93 | 94 | let notes = scanner.parse_nul_terminated_string()?; 95 | 96 | let overall_checksum_expected: u16 = { 97 | let mut c = checksum_region(&solution_bytes, cib_checksum); 98 | c = checksum_region(&solve_state_bytes, c); 99 | c = checksum_metadata_string(&title, c); 100 | c = checksum_metadata_string(&author, c); 101 | c = checksum_metadata_string(©right, c); 102 | for clue in clues.iter() { 103 | c = checksum_clue(clue, c); 104 | } 105 | c = checksum_metadata_string(¬es, c); 106 | c 107 | }; 108 | 109 | if overall_checksum != overall_checksum_expected { 110 | checksum_mismatches.push(ChecksumMismatch { 111 | checksum: Checksum::Overall, 112 | expected: overall_checksum_expected, 113 | actual: overall_checksum, 114 | }) 115 | } 116 | 117 | let solution_checksum = checksum_region(&solution_bytes, 0); 118 | let grid_checksum = checksum_region(&solve_state_bytes, 0); 119 | let partial_board_checksum = { 120 | let mut c = 0; 121 | c = checksum_metadata_string(&title, c); 122 | c = checksum_metadata_string(&author, c); 123 | c = checksum_metadata_string(©right, c); 124 | for clue in clues.iter() { 125 | c = checksum_clue(clue, c); 126 | } 127 | c = checksum_metadata_string(¬es, c); 128 | c 129 | }; 130 | 131 | let expected_masked_checksums = [ 132 | 0x49 ^ (cib_checksum & 0xFF) as u8, 133 | 0x43 ^ (solution_checksum & 0xFF) as u8, 134 | 0x48 ^ (grid_checksum & 0xFF) as u8, 135 | 0x45 ^ (partial_board_checksum & 0xFF) as u8, 136 | 0x41 ^ ((cib_checksum & 0xFF00) >> 8) as u8, 137 | 0x54 ^ ((solution_checksum & 0xFF00) >> 8) as u8, 138 | 0x45 ^ ((grid_checksum & 0xFF00) >> 8) as u8, 139 | 0x44 ^ ((partial_board_checksum & 0xFF00) >> 8) as u8, 140 | ]; 141 | 142 | assert_eq!(expected_masked_checksums.len(), masked_checksums.len()); 143 | for (i, (expected, actual)) in expected_masked_checksums 144 | .iter() 145 | .zip(masked_checksums.iter()) 146 | .enumerate() 147 | { 148 | if expected != actual { 149 | checksum_mismatches.push(ChecksumMismatch { 150 | checksum: Checksum::Masked(i), 151 | expected: *expected as u16, 152 | actual: *actual as u16, 153 | }) 154 | } 155 | } 156 | 157 | let clues = clues 158 | .into_iter() 159 | .map(|clue| decode_str(&clue)) 160 | .collect::, _>>()?; 161 | 162 | let (numbered_squares, clues) = allocate_clues(&solution, &clues); 163 | 164 | let puz = Self { 165 | solution, 166 | solve_state, 167 | title: decode_str(&title)?, 168 | author: decode_str(&author)?, 169 | copyright: decode_str(©right)?, 170 | notes: decode_str(¬es)?, 171 | numbered_squares, 172 | clues, 173 | }; 174 | Ok((puz, checksum_mismatches)) 175 | } 176 | } 177 | 178 | /// Turns a NUL-terminated string into a standard String. Uses UTF-8 first, and then 179 | /// ISO-8859-1 if that fails. 180 | fn decode_str(bytes: &[u8]) -> Result { 181 | assert_eq!(0x0, *bytes.last().unwrap()); 182 | 183 | let bytes = &bytes[0..bytes.len() - 1]; 184 | UTF_8.decode(bytes, Strict).or_else(|_| { 185 | ISO_8859_1.decode(bytes, Strict).map_err(|e| { 186 | Error::EncodingError(format!( 187 | "Failed parsing '{:?}' after trying UTF-8 and ISO-8859: {}", 188 | bytes, e 189 | )) 190 | }) 191 | }) 192 | } 193 | 194 | fn allocate_clues( 195 | grid: &Grid, 196 | clue_list: &[String], 197 | ) -> (HashMap, HashMap) { 198 | let mut clue_number: u8 = 1; 199 | let mut numbered_squares = HashMap::new(); 200 | let mut clues = HashMap::with_capacity(clue_list.len()); 201 | 202 | let mut clue_iter = clue_list.iter(); 203 | 204 | for pos in grid.positions() { 205 | let starts_across = grid.starts_across(pos); 206 | let starts_down = grid.starts_down(pos); 207 | 208 | if starts_across || starts_down { 209 | numbered_squares.insert(pos, clue_number); 210 | 211 | if starts_across { 212 | let clue = clue_iter.next().unwrap(); 213 | clues.insert((clue_number, Across), clue.clone()); 214 | } 215 | 216 | if starts_down { 217 | let clue = clue_iter.next().unwrap(); 218 | clues.insert((clue_number, Down), clue.clone()); 219 | } 220 | 221 | clue_number += 1; 222 | } 223 | } 224 | 225 | (numbered_squares, clues) 226 | } 227 | 228 | // Loosely based on 229 | // https://depth-first.com/articles/2021/12/16/a-beginners-guide-to-parsing-in-rust/ 230 | struct Scanner { 231 | cursor: usize, 232 | data: Vec, 233 | } 234 | 235 | impl Debug for Scanner { 236 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 237 | f.debug_struct("Scanner") 238 | .field("cursor", &self.cursor) 239 | .finish() 240 | } 241 | } 242 | 243 | impl Scanner { 244 | fn new(data: Vec) -> Self { 245 | Self { cursor: 0, data } 246 | } 247 | 248 | /// Consume and return the next byte, or return None if that can't be done. 249 | fn pop(&mut self) -> Result<&u8, Error> { 250 | match self.data.get(self.cursor) { 251 | Some(byte) => { 252 | self.cursor += 1; 253 | Ok(byte) 254 | } 255 | None => Err(Error::EofError(self.cursor)), 256 | } 257 | } 258 | 259 | /// Consume the next two bytes and return them as a `u16`, interpreted as little-endian. 260 | fn parse_short(&mut self) -> Result { 261 | if let Some(b1) = self.data.get(self.cursor) { 262 | if let Some(b2) = self.data.get(self.cursor + 1) { 263 | self.cursor += 2; 264 | return Ok((*b2 as u16) << 8 | (*b1 as u16)); 265 | } 266 | } 267 | Err(Error::EofError(self.cursor)) 268 | } 269 | 270 | /// Take the next `expected.len()` bytes, if they match `expected`. 271 | fn take_exact(&mut self, expected: &[u8]) -> Result<(), Error> { 272 | for (i, expected_byte) in expected.iter().enumerate() { 273 | if let Some(b) = self.data.get(self.cursor + i) { 274 | if b != expected_byte { 275 | return Err(Error::ParseError(format!( 276 | "Expected byte 0x{:X} at position 0x{:X} but got 0x{:X}", 277 | expected_byte, 278 | self.cursor + i, 279 | b 280 | ))); 281 | } 282 | } 283 | } 284 | self.cursor += expected.len(); 285 | Ok(()) 286 | } 287 | 288 | /// Take the next `n`` bytes. 289 | fn take_n_bytes(&mut self, n: usize) -> Result, Error> { 290 | if self.cursor >= self.data.len() { 291 | return Err(Error::EofError(self.cursor)); 292 | } 293 | 294 | if self.cursor + n >= self.data.len() { 295 | return Err(Error::EofError(self.data.len())); 296 | } 297 | 298 | let data = &self.data[self.cursor..self.cursor + n]; 299 | self.cursor += n; 300 | Ok(Vec::from(data)) 301 | } 302 | 303 | /// Parses a C-style NUL-terminated string, including the NUL byte. In 304 | /// this function, we return just the raw bytes as they appear in the 305 | /// file. Converting them to a string is done later. 306 | fn parse_nul_terminated_string(&mut self) -> Result, Error> { 307 | for (index, byte) in self.data[self.cursor..].iter().enumerate() { 308 | if *byte == 0 { 309 | let bytes = &self.data[self.cursor..self.cursor + index + 1]; 310 | self.cursor += index + 1; 311 | return Ok(bytes.to_vec()); 312 | } 313 | } 314 | Err(Error::EofError(self.data.len())) 315 | } 316 | } 317 | 318 | /// Returned when parsing a .puz file succeeded, but one or more of the checksums 319 | /// in the file didn't match the expected value. May indicate a corrupted .puz file, 320 | /// or a bug in this crate. 321 | #[derive(Eq, PartialEq)] 322 | pub struct ChecksumMismatch { 323 | checksum: Checksum, 324 | expected: u16, 325 | actual: u16, 326 | } 327 | 328 | impl Debug for ChecksumMismatch { 329 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 330 | write!( 331 | f, 332 | "Mismatch on checksum {}: Expected {:#x} but got {:#x}", 333 | self.checksum, self.expected, self.actual 334 | ) 335 | } 336 | } 337 | 338 | #[derive(Debug, Eq, PartialEq)] 339 | enum Checksum { 340 | CIB, 341 | Overall, 342 | Masked(usize), 343 | } 344 | 345 | impl Display for Checksum { 346 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 347 | write!(f, "{:?}", self)?; 348 | Ok(()) 349 | } 350 | } 351 | 352 | #[cfg(test)] 353 | mod tests { 354 | use std::collections::HashMap; 355 | use std::fs; 356 | 357 | use super::*; 358 | 359 | #[test] 360 | fn parse_v12_puzzle() { 361 | let data: Vec = fs::read("puzzles/version-1.2-puzzle.puz").unwrap(); 362 | let (puz, checksum_mismatches) = Puz::parse(data).unwrap(); 363 | assert_eq!(checksum_mismatches, []); 364 | assert_eq!(puz.title, "Reference PUZ File"); 365 | assert_eq!(puz.author, "Josh Myer"); 366 | assert_eq!(puz.copyright, "Copyright (c) 2005 Josh Myer"); 367 | assert_eq!(puz.notes, ""); 368 | 369 | #[rustfmt::skip] 370 | assert_eq!(puz.numbered_squares, HashMap::from([ 371 | ((0, 1), 1), 372 | ((1, 0), 2), 373 | ((1, 3), 3), 374 | ((3, 1), 4), 375 | ])); 376 | 377 | #[rustfmt::skip] 378 | assert_eq!( 379 | puz.clues, 380 | HashMap::from([ 381 | ((1, Down), "Pumps your basement".into()), // SUMP 382 | ((2, Across), "I'm ___, thanks for asking\\!".into()), // SUPER 383 | ((3, Down), "Until".into()), // ERE 384 | ((4, Across), "One step short of a pier".into()), // PIE 385 | ]) 386 | ); 387 | 388 | #[rustfmt::skip] 389 | assert_eq!( 390 | puz.solution.to_string(), 391 | concat!( 392 | "\n", 393 | "■S■■■\n", 394 | "SUPER\n", 395 | "■M■R■\n", 396 | "■PIE■\n", 397 | ) 398 | ); 399 | 400 | #[rustfmt::skip] 401 | assert_eq!( 402 | puz.solve_state.to_string(), 403 | concat!( 404 | "\n", 405 | "■S■■■\n", 406 | " U \n", 407 | "■M■ ■\n", 408 | "■P ■\n", 409 | ) 410 | ); 411 | } 412 | } 413 | -------------------------------------------------------------------------------- /crossword/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate is meant to be used as the foundation for a crossword puzzle app. 2 | //! It provides no UI itself, but see `crosstui` for an example of how you can use it 3 | //! to produce a crossword app. 4 | //! 5 | //! Puzzles are loaded from `.puz` files, a de facto standard format for crossword puzzles. 6 | //! You can find `.puz` files to download on many crossword sites. 7 | 8 | use std::cmp::{max, min}; 9 | use std::fmt::{Debug, Display}; 10 | use std::ops::Not; 11 | 12 | use Direction::{Across, Down}; 13 | use puz::Puz; 14 | 15 | mod checksum; 16 | mod puz; 17 | 18 | pub use puz::ChecksumMismatch; 19 | 20 | /// The two crossword directions: `Across` and `Down` 21 | #[derive(Debug, Eq, PartialEq, Hash, Copy, Clone)] 22 | pub enum Direction { 23 | Across, 24 | Down, 25 | } 26 | 27 | impl Not for Direction { 28 | type Output = Self; 29 | fn not(self) -> Self { 30 | match self { 31 | Across => Down, 32 | Down => Across, 33 | } 34 | } 35 | } 36 | 37 | impl Display for Direction { 38 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 39 | match self { 40 | Across => write!(f, "Across"), 41 | Down => write!(f, "Down"), 42 | } 43 | } 44 | } 45 | 46 | impl Direction { 47 | pub fn to_char(&self) -> char { 48 | match self { 49 | Across => 'A', 50 | Down => 'D', 51 | } 52 | } 53 | } 54 | 55 | // The identifier for a clue. For instance, the "12 Down" clue would be represented by the value `(12, Direction::Down)`. 56 | type ClueIdentifier = (u8, Direction); 57 | 58 | /// Represents a crossword puzzle and its cursor (position and direction). 59 | /// When implementing a crossword app, this will be the main structure you will use. 60 | #[derive(Debug)] 61 | pub struct Puzzle { 62 | puz: Puz, 63 | cursor: Cursor, 64 | } 65 | 66 | impl Puzzle { 67 | /// Creates a Puzzle from the bytes of a `.puz` file. 68 | pub fn parse(data: Vec) -> Result<(Self, Vec), Error> { 69 | let (puz, checksum_mismatches) = Puz::parse(data)?; 70 | let cursor = Cursor::from_grid(&puz.solve_state); 71 | let puzzle = Self { puz, cursor }; 72 | Ok((puzzle, checksum_mismatches)) 73 | } 74 | 75 | /// Whether the puzzle is fully filled in, and matches the solution. 76 | pub fn is_solved(&self) -> bool { 77 | self.grid().is_filled() && *self.grid() == self.puz.solution 78 | } 79 | 80 | /// Returns a reference to the current puzzle grid. 81 | pub fn grid(&self) -> &Grid { 82 | &self.puz.solve_state 83 | } 84 | 85 | pub fn title(&self) -> &str { 86 | &self.puz.title 87 | } 88 | 89 | pub fn author(&self) -> &str { 90 | &self.puz.author 91 | } 92 | 93 | pub fn copyright(&self) -> &str { 94 | &self.puz.copyright 95 | } 96 | 97 | pub fn notes(&self) -> &str { 98 | &self.puz.notes 99 | } 100 | 101 | /// Returns a sorted list of all the clues for the given [Direction]. 102 | pub fn clues(&self, direction: Direction) -> Vec<(u8, String)> { 103 | let mut clues: Vec<(u8, String)> = self 104 | .puz 105 | .clues 106 | .iter() 107 | .filter_map(|((num, dir), clue)| { 108 | if *dir == direction { 109 | Some((*num, clue.to_string())) 110 | } else { 111 | None 112 | } 113 | }) 114 | .collect(); 115 | clues.sort_by_key(|(num, _)| *num); 116 | clues 117 | } 118 | 119 | /// Determines how a particular square should be styled. 120 | /// See [SquareStyle]. 121 | pub fn square_style(&self, pos: Pos) -> SquareStyle { 122 | if pos == self.cursor.pos { 123 | return SquareStyle::Cursor; 124 | } 125 | 126 | let (row, col) = pos; 127 | let (cursor_row, cursor_col) = self.cursor.pos; 128 | 129 | if self.cursor.direction == Across && row == cursor_row { 130 | let (col_start, col_end) = (min(col, cursor_col), max(col, cursor_col)); 131 | if (col_start..col_end).any(|c| self.grid().get((row, c)).is_black()) { 132 | return SquareStyle::Standard; 133 | } else { 134 | return SquareStyle::Word; 135 | } 136 | } 137 | 138 | if self.cursor.direction == Down && col == cursor_col { 139 | let (row_start, row_end) = (min(row, cursor_row), max(row, cursor_row)); 140 | if (row_start..row_end).any(|r| self.grid().get((r, col)).is_black()) { 141 | return SquareStyle::Standard; 142 | } else { 143 | return SquareStyle::Word; 144 | } 145 | } 146 | 147 | SquareStyle::Standard 148 | } 149 | 150 | /// The text of the clue for the currently selected word. 151 | pub fn current_clue(&self) -> &str { 152 | self.puz.clues.get(&self.current_clue_identifier()).unwrap() 153 | } 154 | 155 | /// The identifier of the clue for the currently selected word. 156 | pub fn current_clue_identifier(&self) -> ClueIdentifier { 157 | let pos = self.puz.solve_state.get_start(&self.cursor); 158 | let clue_number = *self.puz.numbered_squares.get(&pos).unwrap(); 159 | (clue_number, self.cursor.direction) 160 | } 161 | 162 | /// The identifier of the clue for the word that *would* be selected if the user were to swap the cursor direction. 163 | pub fn cross_clue_identifier(&self) -> Option { 164 | let cursor = Cursor { 165 | pos: self.cursor.pos, 166 | direction: !self.cursor.direction, 167 | }; 168 | if !cursor.is_valid(&self.puz.solve_state) { 169 | return None; 170 | } 171 | 172 | let pos = self.puz.solve_state.get_start(&cursor); 173 | let clue_number = *self.puz.numbered_squares.get(&pos).unwrap(); 174 | Some((clue_number, cursor.direction)) 175 | } 176 | 177 | /// Writes the given letter to the current square. 178 | pub fn add_letter(&mut self, letter: char) { 179 | assert!(letter.is_ascii_alphabetic()); 180 | 181 | self.puz 182 | .solve_state 183 | .set(self.cursor.pos, Square::Letter(letter.to_ascii_uppercase())); 184 | } 185 | 186 | /// Sets the current square to [Empty](Square::Empty). 187 | pub fn erase_letter(&mut self) { 188 | self.puz.solve_state.set(self.cursor.pos, Square::Empty); 189 | } 190 | 191 | /// Moves the cursor back one square, if possible. That is, one square to the left if 192 | /// the current cursor direction is Across, and one square up, if the current direction 193 | /// is down. 194 | pub fn backup_cursor(&mut self) { 195 | self.cursor.backup(&self.puz.solve_state); 196 | } 197 | 198 | /// Moves the cursor to the next empty square in the current word, or if there are no 199 | /// empty squares left, to the start of the next word. 200 | pub fn move_cursor_to_next_empty_in_current_word(&mut self) { 201 | self.cursor 202 | .move_to_next_empty_in_current_word(&self.puz.solve_state); 203 | } 204 | 205 | /// Moves the cursor to the next word in the puzzle. 206 | pub fn advance_cursor_to_next_word(&mut self) { 207 | self.cursor.advance_to_next_word(&self.puz.solve_state); 208 | } 209 | 210 | /// Attempts to swap the cursor direction. However, if the current square is 211 | /// only part of an across clue, the direction cannot be switched to down, 212 | /// and vice versa. 213 | pub fn swap_cursor_direction(&mut self) { 214 | self.cursor.direction = !self.cursor.direction; 215 | self.cursor.adjust_direction(&self.puz.solve_state); 216 | } 217 | 218 | pub fn cursor_up(&mut self) { 219 | self.cursor.up(&self.puz.solve_state); 220 | } 221 | pub fn cursor_down(&mut self) { 222 | self.cursor.down(&self.puz.solve_state); 223 | } 224 | pub fn cursor_left(&mut self) { 225 | self.cursor.left(&self.puz.solve_state); 226 | } 227 | pub fn cursor_right(&mut self) { 228 | self.cursor.right(&self.puz.solve_state); 229 | } 230 | pub fn cursor_direction(&self) -> Direction { 231 | self.cursor.direction 232 | } 233 | } 234 | 235 | /// Indicates how a particular square should look. For instance, [Standard](Self::Standard) 236 | /// might map to white, [Cursor](Self::Cursor) to yellow, and [Word](Self::Word) to gray. 237 | #[derive(Debug)] 238 | pub enum SquareStyle { 239 | /// Default styling 240 | Standard, 241 | /// The cursor is positioned on this square. 242 | Cursor, 243 | /// The cursor is not on this square, but the word indicated by the cursor includes this square. 244 | Word, 245 | } 246 | 247 | /// A square in a crossword grid. 248 | #[derive(Copy, Clone, Eq, PartialEq)] 249 | pub enum Square { 250 | /// A black square where nothing can be entered. 251 | Black, 252 | /// A square where a letter could be entered, but that is currently empty. 253 | Empty, 254 | /// A square with a letter written in it. 255 | Letter(char), 256 | } 257 | 258 | impl Square { 259 | /// Whether this is [Square::Black]. 260 | fn is_black(&self) -> bool { 261 | *self == Self::Black 262 | } 263 | 264 | fn is_empty(&self) -> bool { 265 | *self == Self::Empty 266 | } 267 | 268 | /// Whether this is not a black square, i.e. either a [Square::Empty] or [Square::Letter]. 269 | fn is_white(&self) -> bool { 270 | !self.is_black() 271 | } 272 | } 273 | 274 | impl Debug for Square { 275 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 276 | match self { 277 | Self::Black => write!(f, "■"), 278 | Self::Empty => write!(f, " "), 279 | Self::Letter(c) => write!(f, "{}", c), 280 | }?; 281 | Ok(()) 282 | } 283 | } 284 | 285 | impl Display for Square { 286 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 287 | write!(f, "{:?}", self) 288 | } 289 | } 290 | 291 | impl From<&u8> for Square { 292 | fn from(value: &u8) -> Self { 293 | if *value == b'.' { 294 | Self::Black 295 | } else if *value == b'-' { 296 | Self::Empty 297 | } else { 298 | Self::Letter(*value as char) 299 | } 300 | } 301 | } 302 | 303 | /// A position in a grid: (row, column) 304 | pub type Pos = (usize, usize); 305 | 306 | /// A grid of squares. Used to represent the current state of a partially-solved puzzle, 307 | /// or the solution of a puzzle. 308 | #[derive(Eq, PartialEq)] 309 | pub struct Grid(Vec>); 310 | 311 | impl Grid { 312 | /// Create a new grid from the given bytes. 313 | fn parse(bytes: &[u8], width: usize, height: usize) -> Self { 314 | assert_eq!(bytes.len(), width * height); 315 | 316 | let mut grid = Vec::with_capacity(height); 317 | 318 | for chunk in bytes.chunks(width) { 319 | let row = chunk.iter().map(|b| b.into()).collect::>(); 320 | grid.push(row); 321 | } 322 | 323 | Self(grid) 324 | } 325 | 326 | /// The size of this grid, expressed as (width, height). 327 | fn size(&self) -> (usize, usize) { 328 | (self.width(), self.height()) 329 | } 330 | 331 | /// The width of this grid. 332 | pub fn width(&self) -> usize { 333 | self.0[0].len() 334 | } 335 | 336 | /// The height of this grid. 337 | pub fn height(&self) -> usize { 338 | self.0.len() 339 | } 340 | 341 | /// An iterator over all the positions of this grid, from left to right and top to bottom. 342 | fn positions(&self) -> GridPosIter { 343 | GridPosIter::new(self.size()) 344 | } 345 | 346 | /// Whether this grid is fully filled in -- that is, has no `Square::Empty` in it. 347 | fn is_filled(&self) -> bool { 348 | !self.0.iter().flatten().any(|&sq| sq == Square::Empty) 349 | } 350 | 351 | /// Returns the [Square] at the given [Pos]. 352 | pub fn get(&self, (r, c): Pos) -> Square { 353 | self.0[r][c] 354 | } 355 | 356 | fn set(&mut self, (r, c): Pos, square: Square) { 357 | self.0[r][c] = square; 358 | } 359 | 360 | /// Returns the position of the next white square above `pos`. 361 | fn next_up_neighbor(&self, pos: Pos) -> Option { 362 | let (mut row, col) = pos; 363 | loop { 364 | if row == 0 { 365 | return None; 366 | } 367 | row -= 1; 368 | if self.get((row, col)).is_white() { 369 | return Some((row, col)); 370 | } 371 | } 372 | } 373 | 374 | /// Returns the position of the next white square below `pos`. 375 | fn next_down_neighbor(&self, pos: Pos) -> Option { 376 | let (mut row, col) = pos; 377 | loop { 378 | if row + 1 == self.height() { 379 | return None; 380 | } 381 | row += 1; 382 | if self.get((row, col)).is_white() { 383 | return Some((row, col)); 384 | } 385 | } 386 | } 387 | 388 | /// Returns the position of the next white square to the left of `pos`. 389 | fn next_left_neighbor(&self, pos: Pos) -> Option { 390 | let (row, mut col) = pos; 391 | loop { 392 | if col == 0 { 393 | return None; 394 | } 395 | col -= 1; 396 | if self.get((row, col)).is_white() { 397 | return Some((row, col)); 398 | } 399 | } 400 | } 401 | 402 | /// Returns the position of the next white square to the right of `pos`. 403 | fn next_right_neighbor(&self, pos: Pos) -> Option { 404 | let (row, mut col) = pos; 405 | loop { 406 | if col + 1 == self.width() { 407 | return None; 408 | } 409 | col += 1; 410 | if self.get((row, col)).is_white() { 411 | return Some((row, col)); 412 | } 413 | } 414 | } 415 | 416 | /// Returns the square immediately above the given position, or 417 | /// `Square::Black` if the given position is on the top edge of the grid. 418 | fn up_neighbor(&self, (row, col): Pos) -> Square { 419 | if row == 0 { 420 | Square::Black 421 | } else { 422 | self.get((row - 1, col)) 423 | } 424 | } 425 | 426 | /// Returns the square immediately below the given position, or 427 | /// `Square::Black` if the given position is on the bottom edge of the grid. 428 | fn down_neighbor(&self, (row, col): Pos) -> Square { 429 | if row + 1 == self.height() { 430 | Square::Black 431 | } else { 432 | self.get((row + 1, col)) 433 | } 434 | } 435 | 436 | /// Returns the square immediately to the left of the given position, or 437 | /// `Square::Black` if the given position is on the left edge of the grid. 438 | fn left_neighbor(&self, (row, col): Pos) -> Square { 439 | if col == 0 { 440 | Square::Black 441 | } else { 442 | self.get((row, col - 1)) 443 | } 444 | } 445 | 446 | /// Returns the square immediately to the right of the given position, or 447 | /// `Square::Black` if the given position is on the right edge of the grid. 448 | fn right_neighbor(&self, (row, col): Pos) -> Square { 449 | if col + 1 == self.width() { 450 | Square::Black 451 | } else { 452 | self.get((row, col + 1)) 453 | } 454 | } 455 | 456 | fn starts(&self, pos: Pos, direction: Direction) -> bool { 457 | match direction { 458 | Across => self.starts_across(pos), 459 | Down => self.starts_down(pos), 460 | } 461 | } 462 | 463 | /// Whether the given position is the start of an Across entry. 464 | fn starts_across(&self, pos: Pos) -> bool { 465 | if self.get(pos).is_black() { 466 | return false; 467 | } 468 | 469 | self.left_neighbor(pos).is_black() && self.right_neighbor(pos).is_white() 470 | } 471 | 472 | /// Whether the given position is the start of a Down entry. 473 | fn starts_down(&self, pos: Pos) -> bool { 474 | if self.get(pos).is_black() { 475 | return false; 476 | } 477 | 478 | self.up_neighbor(pos).is_black() && self.down_neighbor(pos).is_white() 479 | } 480 | 481 | /// Determines the position of the start of the word that contains the cursor, 482 | /// and is in the same direction as the cursor. 483 | fn get_start(&self, cursor: &Cursor) -> Pos { 484 | let mut pos = cursor.pos; 485 | match cursor.direction { 486 | Across => loop { 487 | if self.starts_across(pos) { 488 | return pos; 489 | } 490 | let (row, col) = pos; 491 | pos = (row, col - 1); 492 | }, 493 | Down => loop { 494 | if self.starts_down(pos) { 495 | return pos; 496 | } 497 | let (row, col) = pos; 498 | pos = (row - 1, col); 499 | }, 500 | } 501 | } 502 | } 503 | 504 | /// Iterator over all the positions in the grid. 505 | struct GridPosIter { 506 | pos: (usize, usize), 507 | size: (usize, usize), 508 | } 509 | impl GridPosIter { 510 | fn new(size: (usize, usize)) -> Self { 511 | Self { pos: (0, 0), size } 512 | } 513 | } 514 | 515 | impl Iterator for GridPosIter { 516 | type Item = Pos; 517 | fn next(&mut self) -> Option { 518 | let (width, height) = self.size; 519 | let (row, col) = self.pos; 520 | 521 | if row == height { 522 | return None; 523 | } 524 | 525 | if col == width - 1 { 526 | self.pos = (row + 1, 0); 527 | } else { 528 | self.pos = (row, col + 1); 529 | } 530 | 531 | Some((row, col)) 532 | } 533 | } 534 | 535 | impl Debug for Grid { 536 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 537 | for row in &self.0 { 538 | for sq in row { 539 | write!(f, "{}", sq)?; 540 | } 541 | writeln!(f)?; 542 | } 543 | Ok(()) 544 | } 545 | } 546 | 547 | impl Display for Grid { 548 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 549 | write!(f, "\n{:?}", self) 550 | } 551 | } 552 | 553 | /// Represents the position of the user's currently-highlighted square, and the `Direction` 554 | /// of the word they are currently entering. 555 | #[derive(Debug, PartialEq, Eq)] 556 | struct Cursor { 557 | /// The position of the currently-highlighted square. 558 | pos: Pos, 559 | /// The current direction. 560 | direction: Direction, 561 | } 562 | 563 | impl Cursor { 564 | fn from_grid(grid: &Grid) -> Self { 565 | let pos = grid.positions().find(|&p| grid.get(p).is_white()).unwrap(); 566 | 567 | let mut cursor = Self { 568 | pos, 569 | direction: Across, 570 | }; 571 | 572 | cursor.adjust_direction(grid); 573 | 574 | cursor 575 | } 576 | 577 | fn is_valid(&self, grid: &Grid) -> bool { 578 | match self.direction { 579 | Across => { 580 | if grid.right_neighbor(self.pos).is_black() 581 | && grid.left_neighbor(self.pos).is_black() 582 | { 583 | return false; 584 | } 585 | } 586 | Down => { 587 | if grid.up_neighbor(self.pos).is_black() && grid.down_neighbor(self.pos).is_black() 588 | { 589 | return false; 590 | } 591 | } 592 | } 593 | true 594 | } 595 | 596 | fn adjust_direction(&mut self, grid: &Grid) { 597 | if !self.is_valid(grid) { 598 | self.direction = !self.direction; 599 | } 600 | } 601 | 602 | /// Moves the cursor to the next empty square starting from the current one, 603 | /// in the current word. 604 | /// 605 | /// If there are no empty squares at or after the current one, moves to the 606 | /// first empty square in the current word. 607 | /// 608 | /// If there are no empty squares anywhere in the word, advances to start of the next word. 609 | fn move_to_next_empty_in_current_word(&mut self, grid: &Grid) { 610 | if grid.get(self.pos).is_empty() { 611 | return; 612 | } 613 | 614 | let (mut row, mut col) = self.pos; 615 | match self.direction { 616 | Across => loop { 617 | if grid.get((row, col)).is_empty() { 618 | self.pos = (row, col); 619 | return; 620 | } 621 | 622 | // Move one square right, or loop back to the start of the word. 623 | if col == grid.width() - 1 || grid.get((row, col + 1)).is_black() { 624 | (_, col) = grid.get_start(self); 625 | } else { 626 | col += 1; 627 | } 628 | 629 | if (row, col) == self.pos { 630 | self.advance_to_next_word(grid); 631 | return; 632 | } 633 | }, 634 | Down => loop { 635 | if grid.get((row, col)).is_empty() { 636 | self.pos = (row, col); 637 | return; 638 | } 639 | 640 | // Move one square down, or loop back to the start of the word. 641 | if row == grid.height() - 1 || grid.get((row + 1, col)).is_black() { 642 | (row, _) = grid.get_start(self); 643 | } else { 644 | row += 1; 645 | } 646 | 647 | if (row, col) == self.pos { 648 | self.advance_to_next_word(grid); 649 | return; 650 | } 651 | }, 652 | } 653 | } 654 | 655 | /// Moves the cursor to the start of the next word after the current one that is in 656 | /// the same direction as the cursor. If we are already on the last `Across` 657 | /// word, moves to the start of the first `Down` word, and vice versa. 658 | fn advance_to_next_word(&mut self, grid: &Grid) { 659 | let mut iter = GridPosIter { 660 | pos: grid.get_start(self), 661 | size: grid.size(), 662 | }; 663 | 664 | // Skip the start of the current word. 665 | iter.next(); 666 | 667 | for pos in iter { 668 | if grid.starts(pos, self.direction) { 669 | self.pos = pos; 670 | return; 671 | } 672 | } 673 | 674 | // No more words found for the given direction; try the other one. 675 | for pos in grid.positions() { 676 | if grid.starts(pos, !self.direction) { 677 | *self = Cursor { 678 | pos, 679 | direction: !self.direction, 680 | }; 681 | return; 682 | } 683 | } 684 | 685 | unreachable!(); 686 | } 687 | 688 | fn backup(&mut self, grid: &Grid) { 689 | match self.direction { 690 | Across => self.left(grid), 691 | Down => self.up(grid), 692 | } 693 | } 694 | 695 | fn up(&mut self, grid: &Grid) { 696 | if let Some(pos) = grid.next_up_neighbor(self.pos) { 697 | self.pos = pos; 698 | self.adjust_direction(grid); 699 | } 700 | } 701 | 702 | fn down(&mut self, grid: &Grid) { 703 | if let Some(pos) = grid.next_down_neighbor(self.pos) { 704 | self.pos = pos; 705 | self.adjust_direction(grid); 706 | } 707 | } 708 | 709 | fn left(&mut self, grid: &Grid) { 710 | if let Some(pos) = grid.next_left_neighbor(self.pos) { 711 | self.pos = pos; 712 | self.adjust_direction(grid); 713 | } 714 | } 715 | 716 | fn right(&mut self, grid: &Grid) { 717 | if let Some(pos) = grid.next_right_neighbor(self.pos) { 718 | self.pos = pos; 719 | self.adjust_direction(grid); 720 | } 721 | } 722 | } 723 | 724 | /// The errors that may be produced by functions in this crate. 725 | #[derive(Debug)] 726 | pub enum Error { 727 | /// Unexpectedly reached the end of the file at the given byte index. 728 | EofError(usize), 729 | /// Something went wrong while parsing a .puz file. 730 | ParseError(String), 731 | /// Got an error while decoding a string, possibly because it was incorrectly 732 | /// encoded or because this library attempted to use the wrong encoding. 733 | EncodingError(String), 734 | /// The given puz file was marked as "scrambled" which this crate doesn't support. 735 | ScrambledError, 736 | /// An [I/O error](std::io::Error) occurred. 737 | IoError(std::io::Error), 738 | } 739 | 740 | impl From for Error { 741 | fn from(e: std::io::Error) -> Self { 742 | Self::IoError(e) 743 | } 744 | } 745 | 746 | #[cfg(test)] 747 | mod tests { 748 | use super::*; 749 | 750 | fn basic_grid() -> Grid { 751 | let grid_bytes = b"--.---.--.------"; 752 | let grid = Grid::parse(grid_bytes, 4, 4); 753 | 754 | #[rustfmt::skip] 755 | assert_eq!( 756 | grid.to_string(), 757 | concat!( 758 | "\n", 759 | " ■ \n", 760 | " ■ \n", 761 | " ■ \n", 762 | " \n", 763 | ) 764 | ); 765 | 766 | grid 767 | } 768 | 769 | #[test] 770 | fn grid_starts() { 771 | let grid = basic_grid(); 772 | 773 | let across_starts = [(0, 0), (1, 0), (2, 2), (3, 0)]; 774 | let down_starts = [(0, 0), (0, 1), (0, 3), (2, 2)]; 775 | 776 | for pos in grid.positions() { 777 | if across_starts.contains(&pos) { 778 | assert!(grid.starts_across(pos)); 779 | } else { 780 | assert!(!grid.starts_across(pos)); 781 | } 782 | 783 | if down_starts.contains(&pos) { 784 | assert!(grid.starts_down(pos)); 785 | } else { 786 | assert!(!grid.starts_down(pos)); 787 | } 788 | } 789 | 790 | let mut cursor = Cursor::from_grid(&grid); 791 | 792 | for pos in across_starts { 793 | assert_eq!( 794 | cursor, 795 | Cursor { 796 | pos, 797 | direction: Across 798 | } 799 | ); 800 | cursor.advance_to_next_word(&grid); 801 | } 802 | 803 | for pos in down_starts { 804 | assert_eq!( 805 | cursor, 806 | Cursor { 807 | pos, 808 | direction: Down 809 | } 810 | ); 811 | cursor.advance_to_next_word(&grid); 812 | } 813 | } 814 | } 815 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published by 637 | the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "allocator-api2" 16 | version = "0.2.21" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 19 | 20 | [[package]] 21 | name = "anstream" 22 | version = "0.6.19" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" 25 | dependencies = [ 26 | "anstyle", 27 | "anstyle-parse", 28 | "anstyle-query", 29 | "anstyle-wincon", 30 | "colorchoice", 31 | "is_terminal_polyfill", 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle" 37 | version = "1.0.11" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" 40 | 41 | [[package]] 42 | name = "anstyle-parse" 43 | version = "0.2.7" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 46 | dependencies = [ 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle-query" 52 | version = "1.1.3" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" 55 | dependencies = [ 56 | "windows-sys 0.59.0", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-wincon" 61 | version = "3.0.9" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" 64 | dependencies = [ 65 | "anstyle", 66 | "once_cell_polyfill", 67 | "windows-sys 0.59.0", 68 | ] 69 | 70 | [[package]] 71 | name = "anyhow" 72 | version = "1.0.98" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 75 | 76 | [[package]] 77 | name = "atomic" 78 | version = "0.6.1" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" 81 | dependencies = [ 82 | "bytemuck", 83 | ] 84 | 85 | [[package]] 86 | name = "autocfg" 87 | version = "1.4.0" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 90 | 91 | [[package]] 92 | name = "base64" 93 | version = "0.22.1" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 96 | 97 | [[package]] 98 | name = "bit-set" 99 | version = "0.5.3" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" 102 | dependencies = [ 103 | "bit-vec", 104 | ] 105 | 106 | [[package]] 107 | name = "bit-vec" 108 | version = "0.6.3" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" 111 | 112 | [[package]] 113 | name = "bitflags" 114 | version = "1.3.2" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 117 | 118 | [[package]] 119 | name = "bitflags" 120 | version = "2.9.0" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 123 | 124 | [[package]] 125 | name = "block-buffer" 126 | version = "0.10.4" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 129 | dependencies = [ 130 | "generic-array", 131 | ] 132 | 133 | [[package]] 134 | name = "bumpalo" 135 | version = "3.19.0" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 138 | 139 | [[package]] 140 | name = "bytemuck" 141 | version = "1.23.1" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" 144 | 145 | [[package]] 146 | name = "castaway" 147 | version = "0.2.3" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" 150 | dependencies = [ 151 | "rustversion", 152 | ] 153 | 154 | [[package]] 155 | name = "cfg-if" 156 | version = "1.0.0" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 159 | 160 | [[package]] 161 | name = "cfg_aliases" 162 | version = "0.2.1" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 165 | 166 | [[package]] 167 | name = "clap" 168 | version = "4.5.41" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" 171 | dependencies = [ 172 | "clap_builder", 173 | "clap_derive", 174 | ] 175 | 176 | [[package]] 177 | name = "clap_builder" 178 | version = "4.5.41" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" 181 | dependencies = [ 182 | "anstream", 183 | "anstyle", 184 | "clap_lex", 185 | "strsim", 186 | ] 187 | 188 | [[package]] 189 | name = "clap_derive" 190 | version = "4.5.41" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" 193 | dependencies = [ 194 | "heck", 195 | "proc-macro2", 196 | "quote", 197 | "syn 2.0.101", 198 | ] 199 | 200 | [[package]] 201 | name = "clap_lex" 202 | version = "0.7.5" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" 205 | 206 | [[package]] 207 | name = "colorchoice" 208 | version = "1.0.4" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 211 | 212 | [[package]] 213 | name = "compact_str" 214 | version = "0.9.0" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" 217 | dependencies = [ 218 | "castaway", 219 | "cfg-if", 220 | "itoa", 221 | "rustversion", 222 | "ryu", 223 | "static_assertions", 224 | ] 225 | 226 | [[package]] 227 | name = "convert_case" 228 | version = "0.7.1" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" 231 | dependencies = [ 232 | "unicode-segmentation", 233 | ] 234 | 235 | [[package]] 236 | name = "cpufeatures" 237 | version = "0.2.17" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 240 | dependencies = [ 241 | "libc", 242 | ] 243 | 244 | [[package]] 245 | name = "crossterm" 246 | version = "0.29.0" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" 249 | dependencies = [ 250 | "bitflags 2.9.0", 251 | "crossterm_winapi", 252 | "derive_more", 253 | "document-features", 254 | "mio", 255 | "parking_lot", 256 | "rustix", 257 | "signal-hook", 258 | "signal-hook-mio", 259 | "winapi", 260 | ] 261 | 262 | [[package]] 263 | name = "crossterm_winapi" 264 | version = "0.9.1" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 267 | dependencies = [ 268 | "winapi", 269 | ] 270 | 271 | [[package]] 272 | name = "crosstui" 273 | version = "0.2.0" 274 | dependencies = [ 275 | "clap", 276 | "crossterm", 277 | "crossword", 278 | "ratatui", 279 | "ratatui-macros", 280 | ] 281 | 282 | [[package]] 283 | name = "crossword" 284 | version = "0.2.0" 285 | dependencies = [ 286 | "encoding", 287 | ] 288 | 289 | [[package]] 290 | name = "crypto-common" 291 | version = "0.1.6" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 294 | dependencies = [ 295 | "generic-array", 296 | "typenum", 297 | ] 298 | 299 | [[package]] 300 | name = "csscolorparser" 301 | version = "0.6.2" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" 304 | dependencies = [ 305 | "lab", 306 | "phf", 307 | ] 308 | 309 | [[package]] 310 | name = "darling" 311 | version = "0.20.11" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" 314 | dependencies = [ 315 | "darling_core", 316 | "darling_macro", 317 | ] 318 | 319 | [[package]] 320 | name = "darling_core" 321 | version = "0.20.11" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" 324 | dependencies = [ 325 | "fnv", 326 | "ident_case", 327 | "proc-macro2", 328 | "quote", 329 | "strsim", 330 | "syn 2.0.101", 331 | ] 332 | 333 | [[package]] 334 | name = "darling_macro" 335 | version = "0.20.11" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" 338 | dependencies = [ 339 | "darling_core", 340 | "quote", 341 | "syn 2.0.101", 342 | ] 343 | 344 | [[package]] 345 | name = "deltae" 346 | version = "0.3.2" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" 349 | 350 | [[package]] 351 | name = "deranged" 352 | version = "0.4.0" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" 355 | dependencies = [ 356 | "powerfmt", 357 | ] 358 | 359 | [[package]] 360 | name = "derive_more" 361 | version = "2.0.1" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" 364 | dependencies = [ 365 | "derive_more-impl", 366 | ] 367 | 368 | [[package]] 369 | name = "derive_more-impl" 370 | version = "2.0.1" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" 373 | dependencies = [ 374 | "convert_case", 375 | "proc-macro2", 376 | "quote", 377 | "syn 2.0.101", 378 | ] 379 | 380 | [[package]] 381 | name = "digest" 382 | version = "0.10.7" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 385 | dependencies = [ 386 | "block-buffer", 387 | "crypto-common", 388 | ] 389 | 390 | [[package]] 391 | name = "document-features" 392 | version = "0.2.11" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" 395 | dependencies = [ 396 | "litrs", 397 | ] 398 | 399 | [[package]] 400 | name = "either" 401 | version = "1.15.0" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 404 | 405 | [[package]] 406 | name = "encoding" 407 | version = "0.2.33" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" 410 | dependencies = [ 411 | "encoding-index-japanese", 412 | "encoding-index-korean", 413 | "encoding-index-simpchinese", 414 | "encoding-index-singlebyte", 415 | "encoding-index-tradchinese", 416 | ] 417 | 418 | [[package]] 419 | name = "encoding-index-japanese" 420 | version = "1.20141219.5" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" 423 | dependencies = [ 424 | "encoding_index_tests", 425 | ] 426 | 427 | [[package]] 428 | name = "encoding-index-korean" 429 | version = "1.20141219.5" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" 432 | dependencies = [ 433 | "encoding_index_tests", 434 | ] 435 | 436 | [[package]] 437 | name = "encoding-index-simpchinese" 438 | version = "1.20141219.5" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" 441 | dependencies = [ 442 | "encoding_index_tests", 443 | ] 444 | 445 | [[package]] 446 | name = "encoding-index-singlebyte" 447 | version = "1.20141219.5" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" 450 | dependencies = [ 451 | "encoding_index_tests", 452 | ] 453 | 454 | [[package]] 455 | name = "encoding-index-tradchinese" 456 | version = "1.20141219.5" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" 459 | dependencies = [ 460 | "encoding_index_tests", 461 | ] 462 | 463 | [[package]] 464 | name = "encoding_index_tests" 465 | version = "0.1.4" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" 468 | 469 | [[package]] 470 | name = "equivalent" 471 | version = "1.0.2" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 474 | 475 | [[package]] 476 | name = "errno" 477 | version = "0.3.11" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" 480 | dependencies = [ 481 | "libc", 482 | "windows-sys 0.59.0", 483 | ] 484 | 485 | [[package]] 486 | name = "euclid" 487 | version = "0.22.11" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48" 490 | dependencies = [ 491 | "num-traits", 492 | ] 493 | 494 | [[package]] 495 | name = "fancy-regex" 496 | version = "0.11.0" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" 499 | dependencies = [ 500 | "bit-set", 501 | "regex", 502 | ] 503 | 504 | [[package]] 505 | name = "filedescriptor" 506 | version = "0.8.3" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" 509 | dependencies = [ 510 | "libc", 511 | "thiserror 1.0.69", 512 | "winapi", 513 | ] 514 | 515 | [[package]] 516 | name = "finl_unicode" 517 | version = "1.3.0" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "94c970b525906eb37d3940083aa65b95e481fc1857d467d13374e1d925cfc163" 520 | 521 | [[package]] 522 | name = "fixedbitset" 523 | version = "0.4.2" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" 526 | 527 | [[package]] 528 | name = "fnv" 529 | version = "1.0.7" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 532 | 533 | [[package]] 534 | name = "foldhash" 535 | version = "0.1.5" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 538 | 539 | [[package]] 540 | name = "generic-array" 541 | version = "0.14.7" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 544 | dependencies = [ 545 | "typenum", 546 | "version_check", 547 | ] 548 | 549 | [[package]] 550 | name = "getrandom" 551 | version = "0.3.3" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 554 | dependencies = [ 555 | "cfg-if", 556 | "libc", 557 | "r-efi", 558 | "wasi 0.14.2+wasi-0.2.4", 559 | ] 560 | 561 | [[package]] 562 | name = "hashbrown" 563 | version = "0.15.3" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" 566 | dependencies = [ 567 | "allocator-api2", 568 | "equivalent", 569 | "foldhash", 570 | ] 571 | 572 | [[package]] 573 | name = "heck" 574 | version = "0.5.0" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 577 | 578 | [[package]] 579 | name = "hex" 580 | version = "0.4.3" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 583 | 584 | [[package]] 585 | name = "ident_case" 586 | version = "1.0.1" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 589 | 590 | [[package]] 591 | name = "indoc" 592 | version = "2.0.6" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" 595 | 596 | [[package]] 597 | name = "instability" 598 | version = "0.3.7" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "0bf9fed6d91cfb734e7476a06bde8300a1b94e217e1b523b6f0cd1a01998c71d" 601 | dependencies = [ 602 | "darling", 603 | "indoc", 604 | "proc-macro2", 605 | "quote", 606 | "syn 2.0.101", 607 | ] 608 | 609 | [[package]] 610 | name = "is_terminal_polyfill" 611 | version = "1.70.1" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 614 | 615 | [[package]] 616 | name = "itertools" 617 | version = "0.13.0" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 620 | dependencies = [ 621 | "either", 622 | ] 623 | 624 | [[package]] 625 | name = "itertools" 626 | version = "0.14.0" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" 629 | dependencies = [ 630 | "either", 631 | ] 632 | 633 | [[package]] 634 | name = "itoa" 635 | version = "1.0.15" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 638 | 639 | [[package]] 640 | name = "js-sys" 641 | version = "0.3.77" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 644 | dependencies = [ 645 | "once_cell", 646 | "wasm-bindgen", 647 | ] 648 | 649 | [[package]] 650 | name = "kasuari" 651 | version = "0.4.7" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "96d9e0d6a8bf886abccc1cbcac7d74f9000ae5882aedc4a9042188bbc9cd4487" 654 | dependencies = [ 655 | "hashbrown", 656 | "thiserror 2.0.12", 657 | ] 658 | 659 | [[package]] 660 | name = "lab" 661 | version = "0.11.0" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" 664 | 665 | [[package]] 666 | name = "lazy_static" 667 | version = "1.5.0" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 670 | 671 | [[package]] 672 | name = "libc" 673 | version = "0.2.172" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 676 | 677 | [[package]] 678 | name = "line-clipping" 679 | version = "0.3.3" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "51a1679740111eb63b7b4cb3c97b1d5d9f82e142292a25edcfdb4120a48b3880" 682 | dependencies = [ 683 | "bitflags 2.9.0", 684 | ] 685 | 686 | [[package]] 687 | name = "linux-raw-sys" 688 | version = "0.9.4" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 691 | 692 | [[package]] 693 | name = "litrs" 694 | version = "0.4.1" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" 697 | 698 | [[package]] 699 | name = "lock_api" 700 | version = "0.4.12" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 703 | dependencies = [ 704 | "autocfg", 705 | "scopeguard", 706 | ] 707 | 708 | [[package]] 709 | name = "log" 710 | version = "0.4.27" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 713 | 714 | [[package]] 715 | name = "lru" 716 | version = "0.14.0" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "9f8cc7106155f10bdf99a6f379688f543ad6596a415375b36a59a054ceda1198" 719 | dependencies = [ 720 | "hashbrown", 721 | ] 722 | 723 | [[package]] 724 | name = "mac_address" 725 | version = "1.1.8" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" 728 | dependencies = [ 729 | "nix", 730 | "winapi", 731 | ] 732 | 733 | [[package]] 734 | name = "memchr" 735 | version = "2.7.5" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" 738 | 739 | [[package]] 740 | name = "memmem" 741 | version = "0.1.1" 742 | source = "registry+https://github.com/rust-lang/crates.io-index" 743 | checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" 744 | 745 | [[package]] 746 | name = "memoffset" 747 | version = "0.9.1" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" 750 | dependencies = [ 751 | "autocfg", 752 | ] 753 | 754 | [[package]] 755 | name = "minimal-lexical" 756 | version = "0.2.1" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 759 | 760 | [[package]] 761 | name = "mio" 762 | version = "1.0.3" 763 | source = "registry+https://github.com/rust-lang/crates.io-index" 764 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 765 | dependencies = [ 766 | "libc", 767 | "log", 768 | "wasi 0.11.0+wasi-snapshot-preview1", 769 | "windows-sys 0.52.0", 770 | ] 771 | 772 | [[package]] 773 | name = "nix" 774 | version = "0.29.0" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" 777 | dependencies = [ 778 | "bitflags 2.9.0", 779 | "cfg-if", 780 | "cfg_aliases", 781 | "libc", 782 | "memoffset", 783 | ] 784 | 785 | [[package]] 786 | name = "nom" 787 | version = "7.1.3" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 790 | dependencies = [ 791 | "memchr", 792 | "minimal-lexical", 793 | ] 794 | 795 | [[package]] 796 | name = "num-conv" 797 | version = "0.1.0" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 800 | 801 | [[package]] 802 | name = "num-derive" 803 | version = "0.4.2" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" 806 | dependencies = [ 807 | "proc-macro2", 808 | "quote", 809 | "syn 2.0.101", 810 | ] 811 | 812 | [[package]] 813 | name = "num-traits" 814 | version = "0.2.19" 815 | source = "registry+https://github.com/rust-lang/crates.io-index" 816 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 817 | dependencies = [ 818 | "autocfg", 819 | ] 820 | 821 | [[package]] 822 | name = "num_threads" 823 | version = "0.1.7" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" 826 | dependencies = [ 827 | "libc", 828 | ] 829 | 830 | [[package]] 831 | name = "once_cell" 832 | version = "1.21.3" 833 | source = "registry+https://github.com/rust-lang/crates.io-index" 834 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 835 | 836 | [[package]] 837 | name = "once_cell_polyfill" 838 | version = "1.70.1" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 841 | 842 | [[package]] 843 | name = "ordered-float" 844 | version = "4.6.0" 845 | source = "registry+https://github.com/rust-lang/crates.io-index" 846 | checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" 847 | dependencies = [ 848 | "num-traits", 849 | ] 850 | 851 | [[package]] 852 | name = "parking_lot" 853 | version = "0.12.3" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 856 | dependencies = [ 857 | "lock_api", 858 | "parking_lot_core", 859 | ] 860 | 861 | [[package]] 862 | name = "parking_lot_core" 863 | version = "0.9.10" 864 | source = "registry+https://github.com/rust-lang/crates.io-index" 865 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 866 | dependencies = [ 867 | "cfg-if", 868 | "libc", 869 | "redox_syscall", 870 | "smallvec", 871 | "windows-targets", 872 | ] 873 | 874 | [[package]] 875 | name = "pest" 876 | version = "2.8.1" 877 | source = "registry+https://github.com/rust-lang/crates.io-index" 878 | checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" 879 | dependencies = [ 880 | "memchr", 881 | "thiserror 2.0.12", 882 | "ucd-trie", 883 | ] 884 | 885 | [[package]] 886 | name = "pest_derive" 887 | version = "2.8.1" 888 | source = "registry+https://github.com/rust-lang/crates.io-index" 889 | checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" 890 | dependencies = [ 891 | "pest", 892 | "pest_generator", 893 | ] 894 | 895 | [[package]] 896 | name = "pest_generator" 897 | version = "2.8.1" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" 900 | dependencies = [ 901 | "pest", 902 | "pest_meta", 903 | "proc-macro2", 904 | "quote", 905 | "syn 2.0.101", 906 | ] 907 | 908 | [[package]] 909 | name = "pest_meta" 910 | version = "2.8.1" 911 | source = "registry+https://github.com/rust-lang/crates.io-index" 912 | checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" 913 | dependencies = [ 914 | "pest", 915 | "sha2", 916 | ] 917 | 918 | [[package]] 919 | name = "phf" 920 | version = "0.11.3" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" 923 | dependencies = [ 924 | "phf_macros", 925 | "phf_shared", 926 | ] 927 | 928 | [[package]] 929 | name = "phf_codegen" 930 | version = "0.11.3" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" 933 | dependencies = [ 934 | "phf_generator", 935 | "phf_shared", 936 | ] 937 | 938 | [[package]] 939 | name = "phf_generator" 940 | version = "0.11.3" 941 | source = "registry+https://github.com/rust-lang/crates.io-index" 942 | checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" 943 | dependencies = [ 944 | "phf_shared", 945 | "rand", 946 | ] 947 | 948 | [[package]] 949 | name = "phf_macros" 950 | version = "0.11.3" 951 | source = "registry+https://github.com/rust-lang/crates.io-index" 952 | checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" 953 | dependencies = [ 954 | "phf_generator", 955 | "phf_shared", 956 | "proc-macro2", 957 | "quote", 958 | "syn 2.0.101", 959 | ] 960 | 961 | [[package]] 962 | name = "phf_shared" 963 | version = "0.11.3" 964 | source = "registry+https://github.com/rust-lang/crates.io-index" 965 | checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" 966 | dependencies = [ 967 | "siphasher", 968 | ] 969 | 970 | [[package]] 971 | name = "powerfmt" 972 | version = "0.2.0" 973 | source = "registry+https://github.com/rust-lang/crates.io-index" 974 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 975 | 976 | [[package]] 977 | name = "proc-macro2" 978 | version = "1.0.95" 979 | source = "registry+https://github.com/rust-lang/crates.io-index" 980 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 981 | dependencies = [ 982 | "unicode-ident", 983 | ] 984 | 985 | [[package]] 986 | name = "quote" 987 | version = "1.0.40" 988 | source = "registry+https://github.com/rust-lang/crates.io-index" 989 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 990 | dependencies = [ 991 | "proc-macro2", 992 | ] 993 | 994 | [[package]] 995 | name = "r-efi" 996 | version = "5.3.0" 997 | source = "registry+https://github.com/rust-lang/crates.io-index" 998 | checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 999 | 1000 | [[package]] 1001 | name = "rand" 1002 | version = "0.8.5" 1003 | source = "registry+https://github.com/rust-lang/crates.io-index" 1004 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1005 | dependencies = [ 1006 | "rand_core", 1007 | ] 1008 | 1009 | [[package]] 1010 | name = "rand_core" 1011 | version = "0.6.4" 1012 | source = "registry+https://github.com/rust-lang/crates.io-index" 1013 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1014 | 1015 | [[package]] 1016 | name = "ratatui" 1017 | version = "0.30.0-alpha.5" 1018 | source = "registry+https://github.com/rust-lang/crates.io-index" 1019 | checksum = "71365e96fb8f1350c02908e788815c5a57c0c1f557673b274a94edee7a4fe001" 1020 | dependencies = [ 1021 | "instability", 1022 | "ratatui-core", 1023 | "ratatui-crossterm", 1024 | "ratatui-macros", 1025 | "ratatui-termwiz", 1026 | "ratatui-widgets", 1027 | ] 1028 | 1029 | [[package]] 1030 | name = "ratatui-core" 1031 | version = "0.1.0-alpha.6" 1032 | source = "registry+https://github.com/rust-lang/crates.io-index" 1033 | checksum = "f836b2eac888da74162b680a8facdbe784ae73df3b0f711eef74bb90a7477f78" 1034 | dependencies = [ 1035 | "bitflags 2.9.0", 1036 | "compact_str", 1037 | "hashbrown", 1038 | "indoc", 1039 | "itertools 0.14.0", 1040 | "kasuari", 1041 | "lru", 1042 | "strum", 1043 | "thiserror 2.0.12", 1044 | "unicode-segmentation", 1045 | "unicode-truncate", 1046 | "unicode-width", 1047 | ] 1048 | 1049 | [[package]] 1050 | name = "ratatui-crossterm" 1051 | version = "0.1.0-alpha.5" 1052 | source = "registry+https://github.com/rust-lang/crates.io-index" 1053 | checksum = "22f4a90548bf8ed759d226d621d73561110db23aee7b7dc4e12c39ac7132062f" 1054 | dependencies = [ 1055 | "crossterm", 1056 | "instability", 1057 | "ratatui-core", 1058 | ] 1059 | 1060 | [[package]] 1061 | name = "ratatui-macros" 1062 | version = "0.7.0-alpha.4" 1063 | source = "registry+https://github.com/rust-lang/crates.io-index" 1064 | checksum = "1f4c660248a5a9edf95698cf33dc36a82ae48a918594480cdada340d81584e0b" 1065 | dependencies = [ 1066 | "ratatui-core", 1067 | "ratatui-widgets", 1068 | ] 1069 | 1070 | [[package]] 1071 | name = "ratatui-termwiz" 1072 | version = "0.1.0-alpha.5" 1073 | source = "registry+https://github.com/rust-lang/crates.io-index" 1074 | checksum = "4cbb5d7645e56f06ead2a49a72b9cc05022f0b215ec7cdf39d37ed94e9a73d69" 1075 | dependencies = [ 1076 | "ratatui-core", 1077 | "termwiz", 1078 | ] 1079 | 1080 | [[package]] 1081 | name = "ratatui-widgets" 1082 | version = "0.3.0-alpha.5" 1083 | source = "registry+https://github.com/rust-lang/crates.io-index" 1084 | checksum = "388428527811be6da3e23157d951308d9eae4ce1b4d1d545a55673bbcdfb7326" 1085 | dependencies = [ 1086 | "bitflags 2.9.0", 1087 | "hashbrown", 1088 | "indoc", 1089 | "instability", 1090 | "itertools 0.14.0", 1091 | "line-clipping", 1092 | "ratatui-core", 1093 | "strum", 1094 | "time", 1095 | "unicode-segmentation", 1096 | "unicode-width", 1097 | ] 1098 | 1099 | [[package]] 1100 | name = "redox_syscall" 1101 | version = "0.5.12" 1102 | source = "registry+https://github.com/rust-lang/crates.io-index" 1103 | checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" 1104 | dependencies = [ 1105 | "bitflags 2.9.0", 1106 | ] 1107 | 1108 | [[package]] 1109 | name = "regex" 1110 | version = "1.11.1" 1111 | source = "registry+https://github.com/rust-lang/crates.io-index" 1112 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 1113 | dependencies = [ 1114 | "aho-corasick", 1115 | "memchr", 1116 | "regex-automata", 1117 | "regex-syntax", 1118 | ] 1119 | 1120 | [[package]] 1121 | name = "regex-automata" 1122 | version = "0.4.9" 1123 | source = "registry+https://github.com/rust-lang/crates.io-index" 1124 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 1125 | dependencies = [ 1126 | "aho-corasick", 1127 | "memchr", 1128 | "regex-syntax", 1129 | ] 1130 | 1131 | [[package]] 1132 | name = "regex-syntax" 1133 | version = "0.8.5" 1134 | source = "registry+https://github.com/rust-lang/crates.io-index" 1135 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 1136 | 1137 | [[package]] 1138 | name = "rustix" 1139 | version = "1.0.7" 1140 | source = "registry+https://github.com/rust-lang/crates.io-index" 1141 | checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" 1142 | dependencies = [ 1143 | "bitflags 2.9.0", 1144 | "errno", 1145 | "libc", 1146 | "linux-raw-sys", 1147 | "windows-sys 0.59.0", 1148 | ] 1149 | 1150 | [[package]] 1151 | name = "rustversion" 1152 | version = "1.0.20" 1153 | source = "registry+https://github.com/rust-lang/crates.io-index" 1154 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 1155 | 1156 | [[package]] 1157 | name = "ryu" 1158 | version = "1.0.20" 1159 | source = "registry+https://github.com/rust-lang/crates.io-index" 1160 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 1161 | 1162 | [[package]] 1163 | name = "scopeguard" 1164 | version = "1.2.0" 1165 | source = "registry+https://github.com/rust-lang/crates.io-index" 1166 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1167 | 1168 | [[package]] 1169 | name = "serde" 1170 | version = "1.0.219" 1171 | source = "registry+https://github.com/rust-lang/crates.io-index" 1172 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 1173 | dependencies = [ 1174 | "serde_derive", 1175 | ] 1176 | 1177 | [[package]] 1178 | name = "serde_derive" 1179 | version = "1.0.219" 1180 | source = "registry+https://github.com/rust-lang/crates.io-index" 1181 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 1182 | dependencies = [ 1183 | "proc-macro2", 1184 | "quote", 1185 | "syn 2.0.101", 1186 | ] 1187 | 1188 | [[package]] 1189 | name = "sha2" 1190 | version = "0.10.9" 1191 | source = "registry+https://github.com/rust-lang/crates.io-index" 1192 | checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" 1193 | dependencies = [ 1194 | "cfg-if", 1195 | "cpufeatures", 1196 | "digest", 1197 | ] 1198 | 1199 | [[package]] 1200 | name = "signal-hook" 1201 | version = "0.3.17" 1202 | source = "registry+https://github.com/rust-lang/crates.io-index" 1203 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 1204 | dependencies = [ 1205 | "libc", 1206 | "signal-hook-registry", 1207 | ] 1208 | 1209 | [[package]] 1210 | name = "signal-hook-mio" 1211 | version = "0.2.4" 1212 | source = "registry+https://github.com/rust-lang/crates.io-index" 1213 | checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 1214 | dependencies = [ 1215 | "libc", 1216 | "mio", 1217 | "signal-hook", 1218 | ] 1219 | 1220 | [[package]] 1221 | name = "signal-hook-registry" 1222 | version = "1.4.5" 1223 | source = "registry+https://github.com/rust-lang/crates.io-index" 1224 | checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" 1225 | dependencies = [ 1226 | "libc", 1227 | ] 1228 | 1229 | [[package]] 1230 | name = "siphasher" 1231 | version = "1.0.1" 1232 | source = "registry+https://github.com/rust-lang/crates.io-index" 1233 | checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" 1234 | 1235 | [[package]] 1236 | name = "smallvec" 1237 | version = "1.15.0" 1238 | source = "registry+https://github.com/rust-lang/crates.io-index" 1239 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 1240 | 1241 | [[package]] 1242 | name = "static_assertions" 1243 | version = "1.1.0" 1244 | source = "registry+https://github.com/rust-lang/crates.io-index" 1245 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 1246 | 1247 | [[package]] 1248 | name = "strsim" 1249 | version = "0.11.1" 1250 | source = "registry+https://github.com/rust-lang/crates.io-index" 1251 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1252 | 1253 | [[package]] 1254 | name = "strum" 1255 | version = "0.27.1" 1256 | source = "registry+https://github.com/rust-lang/crates.io-index" 1257 | checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" 1258 | dependencies = [ 1259 | "strum_macros", 1260 | ] 1261 | 1262 | [[package]] 1263 | name = "strum_macros" 1264 | version = "0.27.1" 1265 | source = "registry+https://github.com/rust-lang/crates.io-index" 1266 | checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" 1267 | dependencies = [ 1268 | "heck", 1269 | "proc-macro2", 1270 | "quote", 1271 | "rustversion", 1272 | "syn 2.0.101", 1273 | ] 1274 | 1275 | [[package]] 1276 | name = "syn" 1277 | version = "1.0.109" 1278 | source = "registry+https://github.com/rust-lang/crates.io-index" 1279 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1280 | dependencies = [ 1281 | "proc-macro2", 1282 | "quote", 1283 | "unicode-ident", 1284 | ] 1285 | 1286 | [[package]] 1287 | name = "syn" 1288 | version = "2.0.101" 1289 | source = "registry+https://github.com/rust-lang/crates.io-index" 1290 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 1291 | dependencies = [ 1292 | "proc-macro2", 1293 | "quote", 1294 | "unicode-ident", 1295 | ] 1296 | 1297 | [[package]] 1298 | name = "terminfo" 1299 | version = "0.9.0" 1300 | source = "registry+https://github.com/rust-lang/crates.io-index" 1301 | checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" 1302 | dependencies = [ 1303 | "fnv", 1304 | "nom", 1305 | "phf", 1306 | "phf_codegen", 1307 | ] 1308 | 1309 | [[package]] 1310 | name = "termios" 1311 | version = "0.3.3" 1312 | source = "registry+https://github.com/rust-lang/crates.io-index" 1313 | checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" 1314 | dependencies = [ 1315 | "libc", 1316 | ] 1317 | 1318 | [[package]] 1319 | name = "termwiz" 1320 | version = "0.23.3" 1321 | source = "registry+https://github.com/rust-lang/crates.io-index" 1322 | checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" 1323 | dependencies = [ 1324 | "anyhow", 1325 | "base64", 1326 | "bitflags 2.9.0", 1327 | "fancy-regex", 1328 | "filedescriptor", 1329 | "finl_unicode", 1330 | "fixedbitset", 1331 | "hex", 1332 | "lazy_static", 1333 | "libc", 1334 | "log", 1335 | "memmem", 1336 | "nix", 1337 | "num-derive", 1338 | "num-traits", 1339 | "ordered-float", 1340 | "pest", 1341 | "pest_derive", 1342 | "phf", 1343 | "sha2", 1344 | "signal-hook", 1345 | "siphasher", 1346 | "terminfo", 1347 | "termios", 1348 | "thiserror 1.0.69", 1349 | "ucd-trie", 1350 | "unicode-segmentation", 1351 | "vtparse", 1352 | "wezterm-bidi", 1353 | "wezterm-blob-leases", 1354 | "wezterm-color-types", 1355 | "wezterm-dynamic", 1356 | "wezterm-input-types", 1357 | "winapi", 1358 | ] 1359 | 1360 | [[package]] 1361 | name = "thiserror" 1362 | version = "1.0.69" 1363 | source = "registry+https://github.com/rust-lang/crates.io-index" 1364 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 1365 | dependencies = [ 1366 | "thiserror-impl 1.0.69", 1367 | ] 1368 | 1369 | [[package]] 1370 | name = "thiserror" 1371 | version = "2.0.12" 1372 | source = "registry+https://github.com/rust-lang/crates.io-index" 1373 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 1374 | dependencies = [ 1375 | "thiserror-impl 2.0.12", 1376 | ] 1377 | 1378 | [[package]] 1379 | name = "thiserror-impl" 1380 | version = "1.0.69" 1381 | source = "registry+https://github.com/rust-lang/crates.io-index" 1382 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 1383 | dependencies = [ 1384 | "proc-macro2", 1385 | "quote", 1386 | "syn 2.0.101", 1387 | ] 1388 | 1389 | [[package]] 1390 | name = "thiserror-impl" 1391 | version = "2.0.12" 1392 | source = "registry+https://github.com/rust-lang/crates.io-index" 1393 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 1394 | dependencies = [ 1395 | "proc-macro2", 1396 | "quote", 1397 | "syn 2.0.101", 1398 | ] 1399 | 1400 | [[package]] 1401 | name = "time" 1402 | version = "0.3.41" 1403 | source = "registry+https://github.com/rust-lang/crates.io-index" 1404 | checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" 1405 | dependencies = [ 1406 | "deranged", 1407 | "libc", 1408 | "num-conv", 1409 | "num_threads", 1410 | "powerfmt", 1411 | "serde", 1412 | "time-core", 1413 | ] 1414 | 1415 | [[package]] 1416 | name = "time-core" 1417 | version = "0.1.4" 1418 | source = "registry+https://github.com/rust-lang/crates.io-index" 1419 | checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" 1420 | 1421 | [[package]] 1422 | name = "typenum" 1423 | version = "1.18.0" 1424 | source = "registry+https://github.com/rust-lang/crates.io-index" 1425 | checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" 1426 | 1427 | [[package]] 1428 | name = "ucd-trie" 1429 | version = "0.1.7" 1430 | source = "registry+https://github.com/rust-lang/crates.io-index" 1431 | checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" 1432 | 1433 | [[package]] 1434 | name = "unicode-ident" 1435 | version = "1.0.18" 1436 | source = "registry+https://github.com/rust-lang/crates.io-index" 1437 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 1438 | 1439 | [[package]] 1440 | name = "unicode-segmentation" 1441 | version = "1.12.0" 1442 | source = "registry+https://github.com/rust-lang/crates.io-index" 1443 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 1444 | 1445 | [[package]] 1446 | name = "unicode-truncate" 1447 | version = "2.0.0" 1448 | source = "registry+https://github.com/rust-lang/crates.io-index" 1449 | checksum = "8fbf03860ff438702f3910ca5f28f8dac63c1c11e7efb5012b8b175493606330" 1450 | dependencies = [ 1451 | "itertools 0.13.0", 1452 | "unicode-segmentation", 1453 | "unicode-width", 1454 | ] 1455 | 1456 | [[package]] 1457 | name = "unicode-width" 1458 | version = "0.2.0" 1459 | source = "registry+https://github.com/rust-lang/crates.io-index" 1460 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 1461 | 1462 | [[package]] 1463 | name = "utf8parse" 1464 | version = "0.2.2" 1465 | source = "registry+https://github.com/rust-lang/crates.io-index" 1466 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1467 | 1468 | [[package]] 1469 | name = "uuid" 1470 | version = "1.17.0" 1471 | source = "registry+https://github.com/rust-lang/crates.io-index" 1472 | checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" 1473 | dependencies = [ 1474 | "atomic", 1475 | "getrandom", 1476 | "js-sys", 1477 | "wasm-bindgen", 1478 | ] 1479 | 1480 | [[package]] 1481 | name = "version_check" 1482 | version = "0.9.5" 1483 | source = "registry+https://github.com/rust-lang/crates.io-index" 1484 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1485 | 1486 | [[package]] 1487 | name = "vtparse" 1488 | version = "0.6.2" 1489 | source = "registry+https://github.com/rust-lang/crates.io-index" 1490 | checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" 1491 | dependencies = [ 1492 | "utf8parse", 1493 | ] 1494 | 1495 | [[package]] 1496 | name = "wasi" 1497 | version = "0.11.0+wasi-snapshot-preview1" 1498 | source = "registry+https://github.com/rust-lang/crates.io-index" 1499 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1500 | 1501 | [[package]] 1502 | name = "wasi" 1503 | version = "0.14.2+wasi-0.2.4" 1504 | source = "registry+https://github.com/rust-lang/crates.io-index" 1505 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 1506 | dependencies = [ 1507 | "wit-bindgen-rt", 1508 | ] 1509 | 1510 | [[package]] 1511 | name = "wasm-bindgen" 1512 | version = "0.2.100" 1513 | source = "registry+https://github.com/rust-lang/crates.io-index" 1514 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 1515 | dependencies = [ 1516 | "cfg-if", 1517 | "once_cell", 1518 | "rustversion", 1519 | "wasm-bindgen-macro", 1520 | ] 1521 | 1522 | [[package]] 1523 | name = "wasm-bindgen-backend" 1524 | version = "0.2.100" 1525 | source = "registry+https://github.com/rust-lang/crates.io-index" 1526 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 1527 | dependencies = [ 1528 | "bumpalo", 1529 | "log", 1530 | "proc-macro2", 1531 | "quote", 1532 | "syn 2.0.101", 1533 | "wasm-bindgen-shared", 1534 | ] 1535 | 1536 | [[package]] 1537 | name = "wasm-bindgen-macro" 1538 | version = "0.2.100" 1539 | source = "registry+https://github.com/rust-lang/crates.io-index" 1540 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 1541 | dependencies = [ 1542 | "quote", 1543 | "wasm-bindgen-macro-support", 1544 | ] 1545 | 1546 | [[package]] 1547 | name = "wasm-bindgen-macro-support" 1548 | version = "0.2.100" 1549 | source = "registry+https://github.com/rust-lang/crates.io-index" 1550 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 1551 | dependencies = [ 1552 | "proc-macro2", 1553 | "quote", 1554 | "syn 2.0.101", 1555 | "wasm-bindgen-backend", 1556 | "wasm-bindgen-shared", 1557 | ] 1558 | 1559 | [[package]] 1560 | name = "wasm-bindgen-shared" 1561 | version = "0.2.100" 1562 | source = "registry+https://github.com/rust-lang/crates.io-index" 1563 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 1564 | dependencies = [ 1565 | "unicode-ident", 1566 | ] 1567 | 1568 | [[package]] 1569 | name = "wezterm-bidi" 1570 | version = "0.2.3" 1571 | source = "registry+https://github.com/rust-lang/crates.io-index" 1572 | checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" 1573 | dependencies = [ 1574 | "log", 1575 | "wezterm-dynamic", 1576 | ] 1577 | 1578 | [[package]] 1579 | name = "wezterm-blob-leases" 1580 | version = "0.1.1" 1581 | source = "registry+https://github.com/rust-lang/crates.io-index" 1582 | checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" 1583 | dependencies = [ 1584 | "getrandom", 1585 | "mac_address", 1586 | "sha2", 1587 | "thiserror 1.0.69", 1588 | "uuid", 1589 | ] 1590 | 1591 | [[package]] 1592 | name = "wezterm-color-types" 1593 | version = "0.3.0" 1594 | source = "registry+https://github.com/rust-lang/crates.io-index" 1595 | checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" 1596 | dependencies = [ 1597 | "csscolorparser", 1598 | "deltae", 1599 | "lazy_static", 1600 | "wezterm-dynamic", 1601 | ] 1602 | 1603 | [[package]] 1604 | name = "wezterm-dynamic" 1605 | version = "0.2.1" 1606 | source = "registry+https://github.com/rust-lang/crates.io-index" 1607 | checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" 1608 | dependencies = [ 1609 | "log", 1610 | "ordered-float", 1611 | "strsim", 1612 | "thiserror 1.0.69", 1613 | "wezterm-dynamic-derive", 1614 | ] 1615 | 1616 | [[package]] 1617 | name = "wezterm-dynamic-derive" 1618 | version = "0.1.1" 1619 | source = "registry+https://github.com/rust-lang/crates.io-index" 1620 | checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" 1621 | dependencies = [ 1622 | "proc-macro2", 1623 | "quote", 1624 | "syn 1.0.109", 1625 | ] 1626 | 1627 | [[package]] 1628 | name = "wezterm-input-types" 1629 | version = "0.1.0" 1630 | source = "registry+https://github.com/rust-lang/crates.io-index" 1631 | checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" 1632 | dependencies = [ 1633 | "bitflags 1.3.2", 1634 | "euclid", 1635 | "lazy_static", 1636 | "serde", 1637 | "wezterm-dynamic", 1638 | ] 1639 | 1640 | [[package]] 1641 | name = "winapi" 1642 | version = "0.3.9" 1643 | source = "registry+https://github.com/rust-lang/crates.io-index" 1644 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1645 | dependencies = [ 1646 | "winapi-i686-pc-windows-gnu", 1647 | "winapi-x86_64-pc-windows-gnu", 1648 | ] 1649 | 1650 | [[package]] 1651 | name = "winapi-i686-pc-windows-gnu" 1652 | version = "0.4.0" 1653 | source = "registry+https://github.com/rust-lang/crates.io-index" 1654 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1655 | 1656 | [[package]] 1657 | name = "winapi-x86_64-pc-windows-gnu" 1658 | version = "0.4.0" 1659 | source = "registry+https://github.com/rust-lang/crates.io-index" 1660 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1661 | 1662 | [[package]] 1663 | name = "windows-sys" 1664 | version = "0.52.0" 1665 | source = "registry+https://github.com/rust-lang/crates.io-index" 1666 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1667 | dependencies = [ 1668 | "windows-targets", 1669 | ] 1670 | 1671 | [[package]] 1672 | name = "windows-sys" 1673 | version = "0.59.0" 1674 | source = "registry+https://github.com/rust-lang/crates.io-index" 1675 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1676 | dependencies = [ 1677 | "windows-targets", 1678 | ] 1679 | 1680 | [[package]] 1681 | name = "windows-targets" 1682 | version = "0.52.6" 1683 | source = "registry+https://github.com/rust-lang/crates.io-index" 1684 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1685 | dependencies = [ 1686 | "windows_aarch64_gnullvm", 1687 | "windows_aarch64_msvc", 1688 | "windows_i686_gnu", 1689 | "windows_i686_gnullvm", 1690 | "windows_i686_msvc", 1691 | "windows_x86_64_gnu", 1692 | "windows_x86_64_gnullvm", 1693 | "windows_x86_64_msvc", 1694 | ] 1695 | 1696 | [[package]] 1697 | name = "windows_aarch64_gnullvm" 1698 | version = "0.52.6" 1699 | source = "registry+https://github.com/rust-lang/crates.io-index" 1700 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1701 | 1702 | [[package]] 1703 | name = "windows_aarch64_msvc" 1704 | version = "0.52.6" 1705 | source = "registry+https://github.com/rust-lang/crates.io-index" 1706 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1707 | 1708 | [[package]] 1709 | name = "windows_i686_gnu" 1710 | version = "0.52.6" 1711 | source = "registry+https://github.com/rust-lang/crates.io-index" 1712 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1713 | 1714 | [[package]] 1715 | name = "windows_i686_gnullvm" 1716 | version = "0.52.6" 1717 | source = "registry+https://github.com/rust-lang/crates.io-index" 1718 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1719 | 1720 | [[package]] 1721 | name = "windows_i686_msvc" 1722 | version = "0.52.6" 1723 | source = "registry+https://github.com/rust-lang/crates.io-index" 1724 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1725 | 1726 | [[package]] 1727 | name = "windows_x86_64_gnu" 1728 | version = "0.52.6" 1729 | source = "registry+https://github.com/rust-lang/crates.io-index" 1730 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1731 | 1732 | [[package]] 1733 | name = "windows_x86_64_gnullvm" 1734 | version = "0.52.6" 1735 | source = "registry+https://github.com/rust-lang/crates.io-index" 1736 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1737 | 1738 | [[package]] 1739 | name = "windows_x86_64_msvc" 1740 | version = "0.52.6" 1741 | source = "registry+https://github.com/rust-lang/crates.io-index" 1742 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1743 | 1744 | [[package]] 1745 | name = "wit-bindgen-rt" 1746 | version = "0.39.0" 1747 | source = "registry+https://github.com/rust-lang/crates.io-index" 1748 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 1749 | dependencies = [ 1750 | "bitflags 2.9.0", 1751 | ] 1752 | --------------------------------------------------------------------------------