commit a94398d31f39adde628fd77d3105c28927d061ce Author: John Date: Thu Aug 7 05:55:08 2025 -0400 detritus: Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..c460cf0 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,16 @@ +unstable_features = true +max_width = 100 +wrap_comments = true +comment_width = 100 +struct_lit_width = 100 + +imports_granularity = "Crate" +# Allow structs to fill an entire line +# use_small_heuristics = "Max" +# Allow small functions on single line +# fn_single_line = true + +# Alignment +enum_discrim_align_threshold = 12 +#struct_field_align_threshold = 12 +where_single_line = true diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7973b5e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "detritus" +version = "0.1.0" +edition = "2024" + +[dependencies] +repline = { version = "0.0.8", registry = "soft-fish" } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..193efc9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 soft.fish + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..319b5a0 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# detritus + +A prefix tree on T9 dial codes. Perfect for indulging in the benthic zone. + + + +## Why is it named detritus? + +I put in a word, and detritus popped out. diff --git a/res/passthe.jpg b/res/passthe.jpg new file mode 100644 index 0000000..2138d17 Binary files /dev/null and b/res/passthe.jpg differ diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..c7c048a --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,180 @@ +//! Implements a trie on T9 numpads +//! +//! # T9: +//! 1: 2:abc 3:def +//! 4:ghi 5:jkl 6:mno +//! 7:pqrs 8:tuv 9:wxyz + +use std::{ + collections::VecDeque, + ops::{Index, IndexMut}, +}; + +#[derive(Debug, Default)] +pub struct Trie9 { + pub link: Option>, + pub leaf: Vec, +} + +impl Trie9 { + pub fn new() -> Self { + Self::default() + } + + pub fn insert(&mut self, string: &str) -> bool { + let Some(trie) = self.at_mut(string.chars()) else { + return false; + }; + + trie.leaf.push(string.into()); + true + } + + pub fn get(&self, index: T9) -> Option<&Trie9> { + self.link.as_ref()?.get(index as usize) + } + + pub fn get_mut(&mut self, index: T9) -> Option<&mut Trie9> { + self.link.as_mut()?.get_mut(index as usize) + } + + pub fn get_or_insert(&mut self, index: T9) -> &mut Trie9 { + self.link + .get_or_insert_default() + .get_mut(index as usize) + .expect("link exists at all T9 indices") + } + + pub fn at(&self, code: Iter) -> Option<&Self> + where + Iter: IntoIterator, + T: TryInto, + { + let mut code = code.into_iter(); + match code.next() { + None => Some(self), + Some(link) => self.get(link.try_into().ok()?)?.at(code), + } + } + + pub fn at_mut(&mut self, code: Iter) -> Option<&mut Self> + where + Iter: IntoIterator, + T: TryInto, + { + let mut code = code.into_iter(); + match code.next() { + None => Some(self), + Some(link) => self.get_or_insert(link.try_into().ok()?).at_mut(code), + } + } + + pub fn iter<'t>(&'t self) -> Trie9Iter<'t> { + Trie9Iter::new(self) + } +} + +#[derive(Clone, Debug)] +pub struct Trie9Iter<'t9> { + queue: VecDeque<&'t9 Trie9>, + items: std::slice::Iter<'t9, String>, +} + +impl<'t> Trie9Iter<'t> { + pub fn new(trie: &'t Trie9) -> Self { + Self { queue: VecDeque::from([trie]), items: Default::default() } + } +} + +impl<'t> Iterator for Trie9Iter<'t> { + type Item = &'t str; + + fn next(&mut self) -> Option { + let Self { queue, items } = self; + + if let Some(item) = items.next() { + return Some(item.as_str()); + } + + let Trie9 { link, leaf } = queue.pop_front()?; + *items = leaf.iter(); + queue.extend(link.iter().flat_map(|v| v.iter())); + + self.next() + } +} + +impl<'t> IntoIterator for &'t Trie9 { + type IntoIter = Trie9Iter<'t>; + type Item = &'t str; + + fn into_iter(self) -> Self::IntoIter { + Self::IntoIter::new(self) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum T9 { + T2, + T3, + T4, + T5, + T6, + T7, + T8, + T9, +} + +impl From for u8 { + fn from(value: T9) -> Self { + value as u8 + 2 + } +} + +impl From for char { + fn from(value: T9) -> Self { + match value { + T9::T2 => '2', + T9::T3 => '3', + T9::T4 => '4', + T9::T5 => '5', + T9::T6 => '6', + T9::T7 => '7', + T9::T8 => '8', + T9::T9 => '9', + } + } +} + +impl TryFrom for T9 { + type Error = char; + fn try_from(value: char) -> Result { + Ok(match value { + '2' | 'A'..='C' | 'a'..='c' | 'À'..='Æ' | 'à'..='æ' | 'Ç' | 'ç' => Self::T2, + '3' | 'D'..='F' | 'd'..='f' | 'È'..='Ë' | 'è'..='ë' => Self::T3, + '4' | 'G'..='I' | 'g'..='i' | 'Ì'..='Ï' | 'ì'..='ï' => Self::T4, + '5' | 'J'..='L' | 'j'..='l' | '1' => Self::T5, + '6' | 'M'..='O' | 'm'..='o' | 'Ò'..='Ø' | 'ò'..='ø' | '0' => Self::T6, + '7' | 'P'..='S' | 'p'..='s' => Self::T7, + '8' | 'T'..='V' | 't'..='v' | 'Ù'..='Ü' | 'ù'..='ü' => Self::T8, + '9' | 'W'..='Z' | 'w'..='z' => Self::T9, + _ => Err(value)?, + }) + } +} + +impl Index for Trie9 { + type Output = Trie9; + fn index(&self, index: T9) -> &Self::Output { + let Some(link) = self.get(index) else { + panic!("No Trie9 at index {index:?}"); + }; + link + } +} + +impl IndexMut for Trie9 { + fn index_mut(&mut self, index: T9) -> &mut Self::Output { + self.get_or_insert(index) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..1d41308 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,70 @@ +//! Solves a fun little puzzle invented by a coworker + +use detritus::{T9, Trie9}; +use repline::prebaked::*; +use std::{ + error::Error, + fs::File, + io::{BufRead, BufReader, IsTerminal, Write}, +}; + +fn main() -> Result<(), Box> { + let mut trie = Trie9::new(); + + // "Parse" CLI arg + let args: Vec<_> = std::env::args().skip(1).collect(); + let path = match args.as_slice() { + [] => "/usr/share/dict/words", + [path] => path, + _ => Err("Usage: detritus [dict]")?, + }; + + // Read in the database and construct the Trie9 + for line in BufReader::new(File::open(path)?).lines() { + trie.insert(&line?); + } + + if std::io::stdin().is_terminal() { + yestty(trie) + } else { + notty(trie) + } +} + +fn yestty(trie: Trie9) -> Result<(), Box> { + repline::read_and("\x1b[36m", " t>", " ?>", |line| { + let key: Vec<_> = match line.trim().chars().map(T9::try_from).collect() { + Ok(t9) => t9, + Err(e) => { + println!("Unexpected character: {e}"); + return Ok(Response::Deny); + } + }; + + key.iter().for_each(|&c| print!("{}", char::from(c))); + println!(); + + let Some(trie) = trie.at(key.iter().copied()) else { + return Ok(Response::Accept); + }; + for item in trie { + println!("{item}") + } + + Ok(Response::Accept) + })?; + Ok(()) +} + +fn notty(trie: Trie9) -> Result<(), Box> { + let stdin = std::io::stdin().lock(); + let key = std::io::read_to_string(stdin)?; + if let Some(trie) = trie.at(key.trim().chars()) { + for word in trie.iter() { + if writeln!(std::io::stdout(), "{word}").is_err() { + break; + } + } + } + Ok(()) +}