1#![allow(dead_code)]
3
4extern crate alloc;
5use alloc::string::String;
6use alloc::vec::Vec;
7
8const CONNECTION_DATA: &[u8] = include_bytes!("mozc_connection.bin");
9const CONNECTION_HEADER_LEN: usize = 14;
10const CONNECTION_COST_WEIGHT: i32 = 6;
11const MISSING_CONNECTION_INDEX: u16 = 0xFFFF;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum ImeMode {
15 English,
16 Japanese,
17}
18
19static FEP_ACTIVE: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false);
22
23pub fn is_fep_active() -> bool {
25 FEP_ACTIVE.load(core::sync::atomic::Ordering::Relaxed)
26}
27
28pub fn cursor_color() -> u32 {
30 if is_fep_active() {
31 0xFFE03C3C } else {
33 0xFF756CE0 }
35}
36
37#[derive(Debug, Clone)]
38pub struct DictEntry {
39 pub word: String,
40 pub left_id: u16,
41 pub right_id: u16,
42 pub cost: i32,
43}
44
45#[derive(Clone)]
46struct PathState {
47 text: String,
48 cost: i32,
49 right_id: u16,
50}
51
52pub struct ImeState {
53 pub mode: ImeMode,
54 pub dictionary: alloc::collections::BTreeMap<String, Vec<DictEntry>>,
55 pub dict_loaded: bool,
56 pub composing: bool,
57 pub romaji_buf: String,
58 pub kana_buf: String,
59 pub converting: bool,
60 pub candidates: alloc::vec::Vec<String>,
61 pub selected_idx: usize,
62}
63
64impl Default for ImeState {
65 fn default() -> Self {
66 Self::new()
67 }
68}
69
70impl ImeState {
71 pub const fn new() -> Self {
72 Self {
73 mode: ImeMode::English,
74 dictionary: alloc::collections::BTreeMap::new(),
75 dict_loaded: false,
76 composing: false,
77 romaji_buf: String::new(), kana_buf: String::new(),
79 converting: false,
80 candidates: alloc::vec::Vec::new(),
81 selected_idx: 0,
82 }
83 }
84
85 pub fn load_dict(&mut self) {
86 if self.dict_loaded {
87 return;
88 }
89 crate::info!("[SYS] IME: Loading Mozc-based dictionary...");
90 let s = include_str!("mozc_dict.txt");
91 for line in s.lines() {
92 let line = line.trim();
93 if line.is_empty() || line.starts_with('#') {
94 continue;
95 }
96
97 let mut cols = line.split('\t');
98 let Some(yomi) = cols.next() else {
99 continue;
100 };
101 let Some(left_id) = parse_u16(cols.next()) else {
102 continue;
103 };
104 let Some(right_id) = parse_u16(cols.next()) else {
105 continue;
106 };
107 let Some(cost) = parse_i32(cols.next()) else {
108 continue;
109 };
110 let Some(word) = cols.next() else {
111 continue;
112 };
113
114 if yomi.is_empty() || word.is_empty() {
115 continue;
116 }
117
118 self.dictionary
119 .entry(String::from(yomi))
120 .or_default()
121 .push(DictEntry {
122 word: String::from(word),
123 left_id,
124 right_id,
125 cost,
126 });
127 }
128 self.dict_loaded = true;
129 crate::info!(
130 "IME: Loaded Mozc-based dictionary with {} readings",
131 self.dictionary.len()
132 );
133 }
134
135 pub fn toggle_mode(&mut self, to_japanese: bool) {
136 self.load_dict(); if to_japanese {
139 self.mode = ImeMode::Japanese;
140 } else {
141 self.mode = ImeMode::English;
142 }
143 FEP_ACTIVE.store(
144 self.mode == ImeMode::Japanese,
145 core::sync::atomic::Ordering::Relaxed,
146 );
147 self.clear();
148 }
149
150 pub fn clear(&mut self) {
151 self.romaji_buf.clear();
152 self.kana_buf.clear();
153 self.composing = false;
154 self.converting = false;
155 self.candidates.clear();
156 self.selected_idx = 0;
157 }
158
159 pub fn input_char(&mut self, c: char) {
160 if self.mode == ImeMode::English {
161 return;
162 }
163
164 const ROMAJI_SYMBOLS: &[char] = &[
166 '-', ',', '.', '/', '!', '?', '[', ']', '{', '}', '(', ')', '~',
167 ];
168 if c.is_ascii_alphabetic() || ROMAJI_SYMBOLS.contains(&c) {
169 self.romaji_buf.push(c.to_ascii_lowercase());
170 self.composing = true;
171 self.convert_romaji();
172 } else {
173 self.kana_buf.push(c);
174 self.composing = true;
175 }
176 }
177
178 pub fn backspace(&mut self) {
179 if !self.romaji_buf.is_empty() {
180 self.romaji_buf.pop();
181 self.convert_romaji();
182 } else if !self.kana_buf.is_empty() {
183 self.kana_buf.pop();
184 }
185 if self.kana_buf.is_empty() && self.romaji_buf.is_empty() {
186 self.composing = false;
187 self.converting = false;
188 self.candidates.clear();
189 self.selected_idx = 0;
190 }
191 }
192
193 fn convert_romaji(&mut self) {
194 let table = [
195 ("kya", "きゃ"),
197 ("kyu", "きゅ"),
198 ("kyo", "きょ"),
199 ("sha", "しゃ"),
200 ("shu", "しゅ"),
201 ("sho", "しょ"),
202 ("sya", "しゃ"),
203 ("syu", "しゅ"),
204 ("syo", "しょ"),
205 ("shi", "し"),
206 ("cha", "ちゃ"),
207 ("chu", "ちゅ"),
208 ("cho", "ちょ"),
209 ("chi", "ち"),
210 ("tya", "ちゃ"),
211 ("tyu", "ちゅ"),
212 ("tyo", "ちょ"),
213 ("tsu", "つ"),
214 ("tu", "つ"),
215 ("nya", "にゃ"),
216 ("nyu", "にゅ"),
217 ("nyo", "にょ"),
218 ("hya", "ひゃ"),
219 ("hyu", "ひゅ"),
220 ("hyo", "ひょ"),
221 ("mya", "みゃ"),
222 ("myu", "みゅ"),
223 ("myo", "みょ"),
224 ("rya", "りゃ"),
225 ("ryu", "りゅ"),
226 ("ryo", "りょ"),
227 ("gya", "ぎゃ"),
228 ("gyu", "ぎゅ"),
229 ("gyo", "ぎょ"),
230 ("jya", "じゃ"),
231 ("jyu", "じゅ"),
232 ("jyo", "じょ"),
233 ("zya", "じゃ"),
234 ("zyu", "じゅ"),
235 ("zyo", "じょ"),
236 ("dya", "ぢゃ"),
237 ("dyu", "ぢゅ"),
238 ("dyo", "ぢょ"),
239 ("bya", "びゃ"),
240 ("byu", "びゅ"),
241 ("byo", "びょ"),
242 ("pya", "ぴゃ"),
243 ("pyu", "ぴゅ"),
244 ("pyo", "ぴょ"),
245 ("la", "ぁ"),
247 ("li", "ぃ"),
248 ("lu", "ぅ"),
249 ("le", "ぇ"),
250 ("lo", "ぉ"),
251 ("lya", "ゃ"),
252 ("lyu", "ゅ"),
253 ("lyo", "ょ"),
254 ("lwa", "ゎ"),
255 ("ltu", "っ"),
256 ("ltsu", "っ"),
257 ("xa", "ぁ"),
258 ("xi", "ぃ"),
259 ("xu", "ぅ"),
260 ("xe", "ぇ"),
261 ("xo", "ぉ"),
262 ("xya", "ゃ"),
263 ("xyu", "ゅ"),
264 ("xyo", "ょ"),
265 ("xtu", "っ"),
266 ("xtsu", "っ"),
267 ("a", "あ"),
269 ("i", "い"),
270 ("u", "う"),
271 ("e", "え"),
272 ("o", "お"),
273 ("ka", "か"),
275 ("ki", "き"),
276 ("ku", "く"),
277 ("ke", "け"),
278 ("ko", "こ"),
279 ("sa", "さ"),
281 ("si", "し"),
282 ("su", "す"),
283 ("se", "せ"),
284 ("so", "そ"),
285 ("ta", "た"),
287 ("ti", "ち"),
288 ("te", "て"),
289 ("to", "と"),
290 ("na", "な"),
292 ("ni", "に"),
293 ("nu", "ぬ"),
294 ("ne", "ね"),
295 ("no", "の"),
296 ("ha", "は"),
298 ("hi", "ひ"),
299 ("fu", "ふ"),
300 ("hu", "ふ"),
301 ("he", "へ"),
302 ("ho", "ほ"),
303 ("ma", "ま"),
305 ("mi", "み"),
306 ("mu", "む"),
307 ("me", "め"),
308 ("mo", "も"),
309 ("ya", "や"),
311 ("yu", "ゆ"),
312 ("yo", "よ"),
313 ("ra", "ら"),
315 ("ri", "り"),
316 ("ru", "る"),
317 ("re", "れ"),
318 ("ro", "ろ"),
319 ("wa", "わ"),
321 ("wi", "ゐ"),
322 ("we", "ゑ"),
323 ("wo", "を"),
324 ("wha", "うぁ"),
325 ("whi", "うぃ"),
326 ("whe", "うぇ"),
327 ("who", "うぉ"),
328 ("ga", "が"),
330 ("gi", "ぎ"),
331 ("gu", "ぐ"),
332 ("ge", "げ"),
333 ("go", "ご"),
334 ("za", "ざ"),
336 ("ji", "じ"),
337 ("zi", "じ"),
338 ("zu", "ず"),
339 ("ze", "ぜ"),
340 ("zo", "ぞ"),
341 ("ja", "じゃ"),
342 ("ju", "じゅ"),
343 ("jo", "じょ"),
344 ("da", "だ"),
346 ("di", "ぢ"),
347 ("du", "づ"),
348 ("de", "で"),
349 ("do", "ど"),
350 ("ba", "ば"),
352 ("bi", "び"),
353 ("bu", "ぶ"),
354 ("be", "べ"),
355 ("bo", "ぼ"),
356 ("pa", "ぱ"),
358 ("pi", "ぴ"),
359 ("pu", "ぷ"),
360 ("pe", "ぺ"),
361 ("po", "ぽ"),
362 ("va", "ヴぁ"),
364 ("vi", "ヴぃ"),
365 ("vu", "ヴ"),
366 ("ve", "ヴぇ"),
367 ("vo", "ヴぉ"),
368 ("fa", "ふぁ"),
370 ("fi", "ふぃ"),
371 ("fe", "ふぇ"),
372 ("fo", "ふぉ"),
373 ("thi", "てぃ"),
375 ("dhi", "でぃ"),
376 ("dhu", "どぅ"),
377 ("dho", "どぉ"),
378 ("twu", "とぅ"),
379 (",", "、"),
381 (".", "。"),
382 ("/", "・"),
383 ("!", "!"),
384 ("?", "?"),
385 ("[", "「"),
386 ("]", "」"),
387 ("{", "『"),
388 ("}", "』"),
389 ("(", "("),
390 (")", ")"),
391 ("-", "ー"),
393 ("~", "〜"),
394 ];
395
396 let mut temp_kana = String::new();
397 let mut romaji = self.romaji_buf.clone();
398
399 loop {
400 let mut matched = false;
401 for (rom, kana) in table.iter() {
402 if romaji.starts_with(rom) {
403 temp_kana.push_str(kana);
404 romaji = String::from(romaji.get(rom.len()..).unwrap_or(""));
405 matched = true;
406 break;
407 }
408 }
409 if !matched {
410 if romaji.len() > 1
412 && romaji.as_bytes()[0] != b'n'
413 && romaji.as_bytes()[0] == romaji.as_bytes()[1]
414 {
415 temp_kana.push('っ');
416 romaji = String::from(romaji.get(1..).unwrap_or(""));
417 } else if starts_with_n_before_consonant(&romaji) {
418 temp_kana.push('ん');
419 romaji = String::from(romaji.get(1..).unwrap_or(""));
420 } else {
421 break;
422 }
423 }
424 }
425
426 if !temp_kana.is_empty() {
427 self.kana_buf.push_str(&temp_kana);
428 self.romaji_buf = romaji;
429 }
430 }
431
432 fn flush_pending_romaji(&mut self) {
433 if self.romaji_buf == "n" {
434 self.kana_buf.push('ん');
435 self.romaji_buf.clear();
436 }
437 }
438
439 pub fn handle_space(&mut self) -> bool {
440 if !self.composing {
441 return false;
442 }
443
444 self.flush_pending_romaji();
445
446 if !self.converting {
447 self.candidates.clear();
449
450 self.build_conversion_candidates();
451
452 let hiragana = self.kana_buf.clone();
454 if !self.candidates.contains(&hiragana) {
455 self.candidates.push(hiragana);
456 }
457
458 let katakana = to_katakana(&self.kana_buf);
460 if !self.candidates.contains(&katakana) {
461 self.candidates.push(katakana);
462 }
463
464 self.converting = true;
465 self.selected_idx = 0;
466 } else {
467 if !self.candidates.is_empty() {
469 self.selected_idx = (self.selected_idx + 1) % self.candidates.len();
470 }
471 }
472 true
473 }
474
475 fn build_conversion_candidates(&mut self) {
476 const BEAM_WIDTH: usize = 8;
477 const MAX_CANDIDATES: usize = 12;
478 const UNKNOWN_COST: i32 = 12000;
479 const SEGMENT_COST: i32 = 1000;
480
481 let input = self.kana_buf.as_str();
482 let positions = byte_positions(input);
483 if positions.len() <= 1 {
484 return;
485 }
486
487 let mut lattice: Vec<Vec<PathState>> = Vec::new();
488 for _ in 0..positions.len() {
489 lattice.push(Vec::new());
490 }
491 lattice[0].push(PathState {
492 text: String::new(),
493 cost: 0,
494 right_id: 0,
495 });
496
497 for start_idx in 0..positions.len() - 1 {
498 if lattice[start_idx].is_empty() {
499 continue;
500 }
501
502 let start_byte = positions[start_idx];
503 let states = lattice[start_idx].clone();
504
505 for end_idx in start_idx + 1..positions.len() {
506 let end_byte = positions[end_idx];
507 let yomi = input.get(start_byte..end_byte).unwrap_or("");
508
509 if let Some(entries) = self.dictionary.get(yomi) {
510 for state in states.iter() {
511 for entry in entries {
512 let mut text = state.text.clone();
513 text.push_str(&entry.word);
514 let cost = state.cost
515 + adjusted_entry_cost(yomi, entry)
516 + transition_cost(state.right_id, entry.left_id)
517 + SEGMENT_COST;
518 push_beam(
519 &mut lattice[end_idx],
520 PathState {
521 text,
522 cost,
523 right_id: entry.right_id,
524 },
525 BEAM_WIDTH,
526 );
527 }
528 }
529 }
530 }
531
532 let next_byte = positions[start_idx + 1];
533 let unknown = input.get(start_byte..next_byte).unwrap_or("");
534 for state in states.iter() {
535 let mut text = state.text.clone();
536 text.push_str(unknown);
537 push_beam(
538 &mut lattice[start_idx + 1],
539 PathState {
540 text,
541 cost: state.cost + UNKNOWN_COST,
542 right_id: 0,
543 },
544 BEAM_WIDTH,
545 );
546 }
547 }
548
549 let Some(final_states) = lattice.last_mut() else {
550 return;
551 };
552 final_states.sort_by_key(|state| state.cost);
553
554 for state in final_states.iter() {
555 if self.candidates.len() >= MAX_CANDIDATES {
556 break;
557 }
558 if !self.candidates.contains(&state.text) {
559 self.candidates.push(state.text.clone());
560 }
561 }
562 }
563
564 pub fn handle_enter(&mut self) -> Option<String> {
565 self.flush_pending_romaji();
566
567 if self.converting {
568 let res = self
570 .candidates
571 .get(self.selected_idx)
572 .cloned()
573 .unwrap_or_else(|| self.kana_buf.clone());
574 self.clear();
575 Some(res)
576 } else if self.composing {
577 let res = self.kana_buf.clone();
578 self.clear();
579 Some(res)
580 } else {
581 None
582 }
583 }
584
585 pub fn cancel(&mut self) {
587 if self.converting {
588 self.converting = false;
589 self.candidates.clear();
590 self.selected_idx = 0;
591 } else {
593 self.clear();
594 }
595 }
596
597 pub fn select_candidate_by_number(&mut self, n: usize) -> Option<String> {
599 if !self.converting || self.candidates.is_empty() {
600 return None;
601 }
602 let idx = n.saturating_sub(1); if idx < self.candidates.len() {
604 let res = self.candidates[idx].clone();
605 self.clear();
606 Some(res)
607 } else {
608 None
609 }
610 }
611
612 pub fn get_display_text(&self) -> String {
613 if self.converting && !self.candidates.is_empty() {
614 self.candidates[self.selected_idx].clone()
615 } else {
616 self.kana_buf.clone() + &self.romaji_buf
617 }
618 }
619}
620
621pub fn selftest() -> (usize, usize) {
624 let kana_cases: &[(&str, &str)] = &[
626 ("nihon", "にほん"),
627 ("konnichiha", "こんにちは"),
628 ("gakkou", "がっこう"),
629 ("denwa", "でんわ"),
630 ("kanji", "かんじ"),
631 ("kya", "きゃ"),
632 ("sha", "しゃ"),
633 ("chi", "ち"),
634 ("tte", "って"),
635 ("shinbun", "しんぶん"),
636 ("wo", "を"),
637 ("sakka", "さっか"),
638 ("zasshi", "ざっし"),
639 ("annai", "あんない"),
640 ];
641 let conv_cases: &[(&str, &str)] = &[
643 ("nihon", "日本"),
644 ("denwa", "電話"),
645 ("gakkou", "学校"),
646 ("kanji", "漢字"),
647 ];
648
649 let mut passed = 0;
650 let total = kana_cases.len() + conv_cases.len();
651
652 let mut ime = ImeState::new();
654 ime.toggle_mode(true); for (rom, kana) in kana_cases {
657 ime.clear();
658 for c in rom.chars() {
659 ime.input_char(c);
660 }
661 ime.flush_pending_romaji();
662 if &ime.kana_buf == kana {
663 passed += 1;
664 } else {
665 crate::println!(
666 "IME_SELFTEST FAIL: `{}` => `{}` (want `{}`)",
667 rom,
668 ime.kana_buf,
669 kana
670 );
671 }
672 }
673
674 for (rom, kanji) in conv_cases {
675 ime.clear();
676 for c in rom.chars() {
677 ime.input_char(c);
678 }
679 ime.handle_space();
680 if ime.candidates.iter().any(|c| c == kanji) {
681 passed += 1;
682 } else {
683 crate::println!(
684 "IME_SELFTEST FAIL: convert `{}` missing `{}` (got {:?})",
685 rom,
686 kanji,
687 ime.candidates
688 );
689 }
690 }
691
692 (passed, total)
693}
694
695fn parse_u16(s: Option<&str>) -> Option<u16> {
696 s?.parse::<u16>().ok()
697}
698
699fn parse_i32(s: Option<&str>) -> Option<i32> {
700 s?.parse::<i32>().ok()
701}
702
703fn byte_positions(s: &str) -> Vec<usize> {
704 let mut positions = Vec::new();
705 positions.push(0);
706 for (idx, _) in s.char_indices().skip(1) {
707 positions.push(idx);
708 }
709 positions.push(s.len());
710 positions
711}
712
713fn transition_cost(prev_right_id: u16, left_id: u16) -> i32 {
714 mozc_connection_cost(prev_right_id, left_id)
715 .map(|cost| cost / CONNECTION_COST_WEIGHT)
716 .unwrap_or_else(|| {
717 if prev_right_id == 0 || left_id == 0 {
718 0
719 } else if prev_right_id == left_id {
720 40
721 } else {
722 220
723 }
724 })
725}
726
727fn mozc_connection_cost(prev_right_id: u16, left_id: u16) -> Option<i32> {
728 if CONNECTION_DATA.len() < CONNECTION_HEADER_LEN || &CONNECTION_DATA[..8] != b"MOZCCON1" {
729 return None;
730 }
731
732 let context_count = read_u16(CONNECTION_DATA, 8)? as usize;
733 let right_count = read_u16(CONNECTION_DATA, 10)? as usize;
734 let left_count = read_u16(CONNECTION_DATA, 12)? as usize;
735 let prev_right_id = prev_right_id as usize;
736 let left_id = left_id as usize;
737
738 if prev_right_id >= context_count || left_id >= context_count {
739 return None;
740 }
741
742 let right_lookup_offset = CONNECTION_HEADER_LEN;
743 let left_lookup_offset = right_lookup_offset + context_count * 2;
744 let matrix_offset = left_lookup_offset + context_count * 2;
745 let right_index = read_u16(CONNECTION_DATA, right_lookup_offset + prev_right_id * 2)?;
746 let left_index = read_u16(CONNECTION_DATA, left_lookup_offset + left_id * 2)?;
747
748 if right_index == MISSING_CONNECTION_INDEX || left_index == MISSING_CONNECTION_INDEX {
749 return None;
750 }
751
752 let right_index = right_index as usize;
753 let left_index = left_index as usize;
754 if right_index >= right_count || left_index >= left_count {
755 return None;
756 }
757
758 let cost_offset = matrix_offset + (right_index * left_count + left_index) * 2;
759 read_u16(CONNECTION_DATA, cost_offset).map(|cost| cost as i32)
760}
761
762fn adjusted_entry_cost(yomi: &str, entry: &DictEntry) -> i32 {
763 let mut cost = entry.cost;
764 let yomi_len = yomi.chars().count() as i32;
765 if entry.word.chars().all(is_hiragana) && !prefers_hiragana(yomi) {
766 cost += 2500;
767 }
768 if yomi_len == 1 && !entry.word.chars().all(is_hiragana) {
769 cost += 1500;
770 }
771 if yomi_len > 1 && entry.word.chars().all(is_katakana) {
772 cost += 1000;
773 }
774 cost -= (yomi_len - 1).max(0) * 1000;
775 cost
776}
777
778fn push_beam(beam: &mut Vec<PathState>, state: PathState, limit: usize) {
779 if let Some(existing) = beam
780 .iter_mut()
781 .find(|candidate| candidate.text == state.text)
782 {
783 if state.cost < existing.cost {
784 *existing = state;
785 }
786 } else {
787 beam.push(state);
788 }
789
790 beam.sort_by_key(|candidate| candidate.cost);
791 if beam.len() > limit {
792 beam.truncate(limit);
793 }
794}
795
796fn starts_with_n_before_consonant(s: &str) -> bool {
797 if !s.starts_with('n') || s.len() < 2 {
798 return false;
799 }
800
801 let next = s.as_bytes()[1] as char;
802 !matches!(next, 'a' | 'i' | 'u' | 'e' | 'o' | 'y')
805}
806
807fn read_u16(bytes: &[u8], offset: usize) -> Option<u16> {
808 if offset + 1 >= bytes.len() {
809 None
810 } else {
811 Some(u16::from_le_bytes([bytes[offset], bytes[offset + 1]]))
812 }
813}
814
815fn is_hiragana(c: char) -> bool {
816 ('\u{3041}'..='\u{3096}').contains(&c)
817}
818
819fn is_katakana(c: char) -> bool {
820 ('\u{30A1}'..='\u{30FA}').contains(&c) || c == 'ー'
821}
822
823fn prefers_hiragana(s: &str) -> bool {
824 matches!(
825 s,
826 "は" | "が"
827 | "を"
828 | "に"
829 | "へ"
830 | "と"
831 | "で"
832 | "も"
833 | "の"
834 | "や"
835 | "ね"
836 | "よ"
837 | "です"
838 | "ます"
839 | "でした"
840 | "ません"
841 | "ない"
842 | "いい"
843 )
844}
845
846fn to_katakana(s: &str) -> String {
848 let mut res = String::new();
849 for c in s.chars() {
850 if ('\u{3041}'..='\u{3096}').contains(&c) {
851 if let Some(kc) = core::char::from_u32(c as u32 + 0x60) {
852 res.push(kc);
853 } else {
854 res.push(c);
855 }
856 } else {
857 res.push(c);
858 }
859 }
860 res
861}