Skip to content

Commit cc74864

Browse files
fix: polish tray issues and protected words v0.1.192
1 parent f5f0c0e commit cc74864

10 files changed

Lines changed: 116 additions & 11 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "lay"
3-
version = "0.1.191"
3+
version = "0.1.192"
44
edition = "2021"
55
description = "Alpha Double Shift RU/EN layout rescue for Linux desktops"
66
license = "MIT"

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,10 @@ systemctl --user restart lay-daemon
483483

484484
Последние изменения публичной ветки:
485485

486+
- `0.1.192``protected_words.txt` теперь защищает и ASCII-токены, включая
487+
короткие слова вроде `vs`, а раздел `Коррекция` в GNOME tray снова вынесен
488+
отдельным пунктом, а не спрятан внутрь `Арбитра`. `Pause` также доступен
489+
как основной триггер исправления, а не только как прямой RU/EN хоткей.
486490
- `0.1.191` — live-автозамена получила контекстный `layout_en_to_ru` слой:
487491
одиночное `'nj` и смешанный текст вроде `worked 'nj` теперь исправляются в
488492
`это`, но технические хвосты вроде `git`, `wi-fi`, `status;` не запускают

extension/lay@radislabus-star.github.io/lay-impl.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ const STATS_PATH = GLib.get_home_dir() + '/.local/share/lay/stats.json';
2222
const RECENT_ACTIONS_PATH = GLib.get_home_dir() + '/.local/share/lay/recent_actions.jsonl';
2323
const PROJECT_DIR = GLib.get_home_dir() + '/projects/lay';
2424
const UPDATE_LOG_PATH = GLib.get_home_dir() + '/.local/state/lay/update.log';
25-
const APP_VERSION = '0.1.191';
25+
const APP_VERSION = '0.1.192';
2626
const APP_DESCRIPTION = 'Alpha RU/EN layout helper: double Shift и помощь при наборе';
27-
const APP_RELEASE_DATE = '2026-05-22';
27+
const APP_RELEASE_DATE = '2026-05-23';
2828
const APP_LICENSE = 'MIT';
2929
const APP_URL = 'https://github.com/radislabus-star/lay-public';
3030
const APP_PLATFORM = 'Linux desktops: GNOME, KDE, Wayland, X11';
@@ -657,6 +657,7 @@ class LayIndicator extends PanelMenu.Button {
657657

658658
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
659659
this.menu.addMenuItem(this._arbiterMenu());
660+
this.menu.addMenuItem(this._correctionPipelineMenu());
660661
this.menu.addMenuItem(this._recentActionsMenu());
661662
this.menu.addMenuItem(this._ptahAlexsMenu());
662663
this.menu.addMenuItem(this._triggerMenu());
@@ -784,8 +785,6 @@ class LayIndicator extends PanelMenu.Button {
784785
restartDaemon();
785786
}],
786787
], this._backendButtons));
787-
item.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
788-
item.menu.addMenuItem(this._correctionPipelineMenu());
789788
return item;
790789
}
791790

@@ -1265,6 +1264,7 @@ class LayIndicator extends PanelMenu.Button {
12651264
['single-rshift', 'RShift'],
12661265
['single-rctrl', 'RCtrl'],
12671266
['single-ralt', 'RAlt'],
1267+
['single-pause', 'Pause'],
12681268
]) {
12691269
const row = new PopupMenu.PopupMenuItem(label);
12701270
row.connect('activate', () => this._setTrigger(id));

extension/lay@radislabus-star.github.io/metadata.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"uuid": "lay@radislabus-star.github.io",
33
"name": "Lay Layout Switcher",
4-
"version": 191,
5-
"version-name": "0.1.191",
4+
"version": 192,
5+
"version-name": "0.1.192",
66
"description": "Alpha GNOME Shell extension для lay-daemon. Показывает EN/RU в трее, переключает раскладку по клику. Минимальный DBus bridge для layout activation и fallback text insert.",
77
"shell-version": ["45", "46", "47", "50"],
88
"url": "https://github.com/radislabus-star/lay-public"

src/bin/lay_daemon/tests/typing_assist_rules.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,15 @@ fn typing_assist_auto_switch_keeps_english_and_protected_ascii() {
166166
assert_eq!(apply_typing_assist("grep --color=auto ", true), None);
167167
}
168168

169+
#[test]
170+
fn typing_assist_keeps_user_protected_ascii_words_when_configured() {
171+
if std::env::var_os("LAY_TEST_USER_PROTECTED_ASCII").is_none() {
172+
return;
173+
}
174+
175+
assert_eq!(apply_typing_assist("vs ", true), None);
176+
}
177+
169178
#[test]
170179
fn typing_assist_pipeline_can_disable_rules() {
171180
let no_en_to_ru = typing_pipeline_with_disabled(&["layout_en_to_ru"]);

src/layout_autoswitch.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ use std::collections::HashSet;
88
use std::sync::OnceLock;
99

1010
use crate::keyboard::is_cyrillic_letter;
11-
use crate::lexicon::{is_common_en_technical_word, is_ru_hyphen_particle, EN_HUNSPELL, EN_WORDS};
11+
use crate::lexicon::{
12+
extend_user_protected_ascii_words, is_common_en_technical_word, is_ru_hyphen_particle,
13+
EN_HUNSPELL, EN_WORDS,
14+
};
1215
use crate::phrase_lexicon::is_common_short_russian_preposition;
1316
use crate::ru_typo::has_plausible_russian_typo_candidate;
1417
use crate::russian_chars::is_russian_vowel;
@@ -382,6 +385,7 @@ fn english_dictionary() -> &'static HashSet<String> {
382385
if let Ok(extra) = load_ascii_word_list_min_len(EN_WORDS, 4) {
383386
words.extend(extra);
384387
}
388+
extend_user_protected_ascii_words(&mut words, 1);
385389
words
386390
})
387391
}

src/lexicon.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
//! sets so runtime checks stay cheap and platform-neutral.
66
77
use std::collections::HashSet;
8+
use std::path::Path;
89
use std::sync::OnceLock;
910

1011
pub const RU_HUNSPELL: &str = "/usr/share/hunspell/ru_RU.dic";
@@ -37,6 +38,7 @@ pub fn warm_up() {
3738
let _ = ru_hyphen_particles().len();
3839
let _ = visual_b_default_replacement();
3940
let _ = visual_b_after_ascii_replacement();
41+
let _ = user_protected_ascii_words().len();
4042
}
4143

4244
pub fn is_common_ru_word(word: &str) -> bool {
@@ -79,10 +81,39 @@ pub fn visual_b_after_ascii_replacement() -> &'static str {
7981
first_data_word(VISUAL_B_AFTER_ASCII_DATA)
8082
}
8183

84+
pub fn is_user_protected_ascii_word(word: &str) -> bool {
85+
if !word.is_ascii() {
86+
return false;
87+
}
88+
user_protected_ascii_words().contains(&word.to_ascii_lowercase())
89+
}
90+
91+
pub fn extend_user_protected_ascii_words(words: &mut HashSet<String>, min_chars: usize) {
92+
words.extend(
93+
user_protected_ascii_words()
94+
.iter()
95+
.filter(|word| word.chars().count() >= min_chars)
96+
.cloned(),
97+
);
98+
}
99+
82100
pub fn extend_common_ru_words(words: &mut HashSet<String>) {
83101
words.extend(common_ru_words().iter().cloned());
84102
}
85103

104+
fn user_protected_ascii_words() -> &'static HashSet<String> {
105+
static WORDS: OnceLock<HashSet<String>> = OnceLock::new();
106+
WORDS.get_or_init(|| {
107+
let Some(home) = std::env::var_os("HOME") else {
108+
return HashSet::new();
109+
};
110+
let path = std::path::PathBuf::from(home).join(PROTECTED_WORDS_PATH);
111+
load_plain_words(&path)
112+
.map(|words| ascii_words_from_iter(words, 1))
113+
.unwrap_or_default()
114+
})
115+
}
116+
86117
fn common_ru_words() -> &'static HashSet<String> {
87118
static WORDS: OnceLock<HashSet<String>> = OnceLock::new();
88119
WORDS.get_or_init(|| parse_word_data(COMMON_RU_DATA))
@@ -131,6 +162,39 @@ fn parse_word_data(data: &str) -> HashSet<String> {
131162
.collect()
132163
}
133164

165+
#[cfg(test)]
166+
pub(crate) fn parse_ascii_word_data(data: &str, min_chars: usize) -> HashSet<String> {
167+
ascii_words_from_iter(
168+
data.lines()
169+
.map(str::trim)
170+
.filter(|line| !line.is_empty() && !line.starts_with('#'))
171+
.map(str::to_string),
172+
min_chars,
173+
)
174+
}
175+
176+
fn ascii_words_from_iter<I>(words: I, min_chars: usize) -> HashSet<String>
177+
where
178+
I: IntoIterator<Item = String>,
179+
{
180+
words
181+
.into_iter()
182+
.map(|word| word.trim().to_ascii_lowercase())
183+
.filter(|word| word.chars().count() >= min_chars)
184+
.filter(|word| {
185+
word.is_ascii()
186+
&& word
187+
.chars()
188+
.all(|ch| ch.is_ascii_alphabetic() || ch == '-' || ch == '_')
189+
})
190+
.collect()
191+
}
192+
193+
fn load_plain_words(path: &Path) -> std::io::Result<HashSet<String>> {
194+
let text = std::fs::read_to_string(path)?;
195+
Ok(parse_word_data(&text))
196+
}
197+
134198
fn first_data_word(data: &'static str) -> &'static str {
135199
data.lines()
136200
.map(str::trim)

src/lexicon_tests.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,24 @@ fn lexical_data_loads_without_code_word_lists() {
1515
assert_eq!(visual_b_after_ascii_replacement(), "и");
1616
assert!(!is_common_en_technical_word("hello"));
1717
}
18+
19+
#[test]
20+
fn protected_ascii_words_parser_keeps_short_user_tokens() {
21+
let words = parse_ascii_word_data(
22+
r#"
23+
# comments are ignored
24+
vs
25+
WPS
26+
AmoCRM
27+
привет
28+
wi-fi
29+
"#,
30+
1,
31+
);
32+
33+
assert!(words.contains("vs"));
34+
assert!(words.contains("wps"));
35+
assert!(words.contains("amocrm"));
36+
assert!(words.contains("wi-fi"));
37+
assert!(!words.contains("привет"));
38+
}

src/word_recognizer.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ use std::sync::OnceLock;
88

99
use crate::keyboard::is_cyrillic_letter;
1010
use crate::lexicon::{
11-
is_common_en_technical_word, is_common_ru_word, EN_HUNSPELL, EN_WORDS, RU_HUNSPELL,
11+
extend_user_protected_ascii_words, is_common_en_technical_word, is_common_ru_word,
12+
is_user_protected_ascii_word, EN_HUNSPELL, EN_WORDS, RU_HUNSPELL,
1213
};
1314
use crate::word_reader::split_word_punctuation;
1415

@@ -239,7 +240,8 @@ pub fn is_protected_ascii_token(core: &str) -> bool {
239240
return false;
240241
}
241242
core.is_ascii()
242-
&& (has_domain_like_dot(core)
243+
&& (is_user_protected_ascii_word(core)
244+
|| has_domain_like_dot(core)
243245
|| core.contains('@')
244246
|| core.contains("://")
245247
|| core.contains('/')
@@ -355,6 +357,7 @@ fn english_words() -> &'static HashSet<String> {
355357
.map(|word| word.to_ascii_lowercase()),
356358
);
357359
}
360+
extend_user_protected_ascii_words(&mut words, 1);
358361
words
359362
})
360363
}

0 commit comments

Comments
 (0)