Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,14 @@ cargo +nightly test test_name -- --nocapture
```

Test modules cover: config I/O, config validation, hotkey formatting, keyboard sequences, character mapping invariants, ring buffer.

## Agent Workflow Requirements

When making changes in this repository, always:

- Run formatting, linting, build, and tests before reporting results:
- `cargo +nightly fmt --check`
- `cargo +nightly clippy --all-targets --all-features -- -D warnings`
- `cargo +nightly build --features debug-tracing`
- `cargo +nightly test --locked`
- Address and fix any findings from these checks before finalizing work.
49 changes: 47 additions & 2 deletions src/domain/text/convert.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::{ptr::null_mut, thread, time::Duration};

use mapping::convert_ru_en_bidirectional;
use mapping::{ConversionDirection, conversion_direction_for_text, convert_ru_en_with_direction};
use windows::Win32::{
Foundation::{HWND, LPARAM, WPARAM},
System::DataExchange::GetClipboardSequenceNumber,
Expand Down Expand Up @@ -159,7 +159,10 @@ fn convert_selection_from_text(
) -> Result<(), ConvertSelectionError> {
let delay_ms = crate::helpers::get_edit_u32(state.edits.delay_ms).unwrap_or(100);

let converted = convert_ru_en_bidirectional(text);
let direction = conversion_direction_for_text(text)
.or_else(expected_direction_for_foreground_window)
.unwrap_or(ConversionDirection::RuToEn);
let converted = convert_ru_en_with_direction(text, direction);
let converted_units = converted.encode_utf16().count();

thread::sleep(Duration::from_millis(u64::from(delay_ms)));
Expand Down Expand Up @@ -222,6 +225,48 @@ fn current_layout_for_window(fg: HWND) -> HKL {
}
}

#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum LayoutLanguage {
English,
Russian,
}

const LANG_ID_PRIMARY_MASK: u16 = 0x03ff;
const LANG_ENGLISH: u16 = 0x0009;
const LANG_RUSSIAN: u16 = 0x0019;

fn lang_id_from_hkl(hkl: HKL) -> u16 {
(hkl.0 as usize & 0xffff) as u16
}

fn primary_lang_id(lang_id: u16) -> u16 {
lang_id & LANG_ID_PRIMARY_MASK
}

/// Returns the active language of the layout (based on LANGID in HKL).
fn active_layout_language(hkl: HKL) -> Option<LayoutLanguage> {
match primary_lang_id(lang_id_from_hkl(hkl)) {
LANG_RUSSIAN => Some(LayoutLanguage::Russian),
LANG_ENGLISH => Some(LayoutLanguage::English),
_ => None,
}
}

/// Returns the expected conversion direction for a given keyboard layout.
fn expected_direction_for_layout(hkl: HKL) -> Option<ConversionDirection> {
match active_layout_language(hkl) {
Some(LayoutLanguage::Russian) => Some(ConversionDirection::RuToEn),
Some(LayoutLanguage::English) => Some(ConversionDirection::EnToRu),
None => None,
}
}

pub(crate) fn expected_direction_for_foreground_window() -> Option<ConversionDirection> {
let fg = foreground_window()?;
let layout = current_layout_for_window(fg);
expected_direction_for_layout(layout)
}

/// Enumerates installed keyboard layouts for the current desktop.
///
/// Returns an empty vector when enumeration fails or yields no results.
Expand Down
35 changes: 23 additions & 12 deletions src/domain/text/last_word.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ use windows::Win32::UI::{
Input::KeyboardAndMouse::VIRTUAL_KEY, WindowsAndMessaging::GetForegroundWindow,
};

use super::{mapping::convert_ru_en_bidirectional, switch_keyboard_layout, wait_shift_released};
use super::{
convert::expected_direction_for_foreground_window,
mapping::{ConversionDirection, conversion_direction_for_text, convert_ru_en_with_direction},
switch_keyboard_layout, wait_shift_released,
};
use crate::{
app::AppState,
conversion::input::{KeySequence, send_text_unicode},
Expand All @@ -29,6 +33,13 @@ const MIN_CONFIDENCE_GAIN: f64 = 0.25;

static AUTOCONVERT_IN_PROGRESS: AtomicBool = AtomicBool::new(false);

fn convert_with_layout_fallback(text: &str) -> String {
let direction = conversion_direction_for_text(text)
.or_else(expected_direction_for_foreground_window)
.unwrap_or(ConversionDirection::RuToEn);
convert_ru_en_with_direction(text, direction)
}

pub fn convert_last_word(state: &mut AppState) {
convert_last_word_impl(state, true);
}
Expand Down Expand Up @@ -277,7 +288,7 @@ fn autoconvert_candidate(p: &LastWordPayload) -> Result<String, SkipReason> {

let (word_core, word_punct) = split_trailing_convertible_punct(&p.word);

let converted_core = convert_ru_en_bidirectional(word_core);
let converted_core = convert_with_layout_fallback(word_core);

let mut converted = String::with_capacity(converted_core.len() + word_punct.len());
converted.push_str(&converted_core);
Expand Down Expand Up @@ -505,7 +516,7 @@ fn convert_last_word_impl(state: &mut AppState, switch_layout: bool) {
return;
}

let converted = convert_ru_en_bidirectional(&payload.word);
let converted = convert_with_layout_fallback(&payload.word);
tracing::trace!(%converted, "converted");

if let Err(err) = apply_last_word_replacement(&payload, &converted) {
Expand Down Expand Up @@ -677,7 +688,7 @@ mod tests {
let detector = detector_ru_en();

let word = "привет";
let converted = convert_ru_en_bidirectional(word);
let converted = convert_with_layout_fallback(word);
assert_eq!(converted, "ghbdtn");

let decision = should_autoconvert_word(&detector, word, &converted);
Expand All @@ -692,7 +703,7 @@ mod tests {
let detector = detector_ru_en();

let word = "ghbdtn";
let converted = convert_ru_en_bidirectional(word);
let converted = convert_with_layout_fallback(word);
assert_eq!(converted, "привет");

match should_autoconvert_word(&detector, word, &converted) {
Expand All @@ -708,7 +719,7 @@ mod tests {
let detector = detector_ru_en();

let word = "world";
let converted = convert_ru_en_bidirectional(word);
let converted = convert_with_layout_fallback(word);
assert_ne!(converted, word);

let decision = should_autoconvert_word(&detector, word, &converted);
Expand All @@ -723,7 +734,7 @@ mod tests {
let detector = detector_ru_en();

let word = "rfr"; // "как"
let converted = convert_ru_en_bidirectional(word);
let converted = convert_with_layout_fallback(word);
assert_eq!(converted, "как");

let decision = should_autoconvert_word(&detector, word, &converted);
Expand All @@ -738,7 +749,7 @@ mod tests {
let detector = detector_ru_en();

let word = ";tklf"; // starts with punctuation, should fail script heuristics
let converted = convert_ru_en_bidirectional(word);
let converted = convert_with_layout_fallback(word);
assert_ne!(converted, word);

let decision = should_autoconvert_word(&detector, word, &converted);
Expand Down Expand Up @@ -774,7 +785,7 @@ mod tests {
let detector = detector_ru_en();

let word = "hellp";
let converted = convert_ru_en_bidirectional(word);
let converted = convert_with_layout_fallback(word);
assert_ne!(converted, word);

let decision = should_autoconvert_word(&detector, word, &converted);
Expand All @@ -786,7 +797,7 @@ mod tests {
let detector = detector_ru_en();

let word = "hjyxnmyuj";
let converted = convert_ru_en_bidirectional(word);
let converted = convert_with_layout_fallback(word);
assert_eq!(converted, "рончтьнго");

match should_autoconvert_word(&detector, word, &converted) {
Expand Down Expand Up @@ -821,7 +832,7 @@ mod tests {
];

for (word, should_convert) in cases {
let converted = convert_ru_en_bidirectional(word);
let converted = convert_with_layout_fallback(word);

let decision = should_autoconvert_word(&detector, word, &converted);
match (should_convert, decision) {
Expand All @@ -847,7 +858,7 @@ mod tests {
assert_eq!(p.word.as_str(), "ghbdtn,");
assert_eq!(p.suffix.as_str(), " \t");

let converted = convert_ru_en_bidirectional(&p.word);
let converted = convert_with_layout_fallback(&p.word);
let decision = should_autoconvert_word(&detector, &p.word, &converted);

assert!(
Expand Down
Loading
Loading