Skip to content
Draft
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
7 changes: 7 additions & 0 deletions src/lotus-config.h
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,13 @@ namespace fcitx {
Option<bool> freeMarking{this, "FreeMarking", _("Allow Type With More Freedom"), true};
Option<bool> ddFreeStyle{this, "DdFreeStyle", _("Allow dd To Produce đ When Auto Restore Invalid Words Is On"), true};
Option<bool> fixUinputWithAck{this, "FixUinputWithAck", _("Fix Uinput Mode With Ack"), false};
Option<bool> waitSurroundingEvent{this, "WaitSurroundingEvent", _("Experiment: after uinput backspaces, wait for the surrounding-text event instead of sleeping"), false};
Option<int> waitSurroundingTimeoutMs{this, "WaitSurroundingTimeoutMs", _("Experiment: timeout (ms) for that wait"), 50};
Option<int> waitSurroundingShortMs{this, "WaitSurroundingShortMs", _("Experiment: shorter timeout (ms) used after two consecutive timeouts without a matching event"), 40};
Option<int> waitSurroundingProbeEvery{this, "WaitSurroundingProbeEvery", _("Experiment: when the app's snapshot is frozen during deletion, stop waiting and re-probe once every N replacements"), 4};
Option<int> waitSurroundingMinPerKeyMs{this, "WaitSurroundingMinPerKeyMs", _("Experiment: a snapshot identical to the one at send time counts as done only after this many ms per backspace"), 8};
Option<int> surrDeleteSleepMs{this, "SurrDeleteSleepMs", _("Experiment: ms per deleted char to sleep after deleteSurroundingText"), 4};
Option<int> surrCommitSleepMs{this, "SurrCommitSleepMs", _("Experiment: ms per committed char to sleep after commitString on the surrounding-text path"), 3};
Option<bool> useLotusIcons{this, "UseLotusIcons", _("Use Lotus Status Icons"), false};

Option<bool> enableDictionary{this, "EnableDictionary", _("Custom Dictionary"), false};
Expand Down
1 change: 1 addition & 0 deletions src/lotus-engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,7 @@ namespace fcitx {
auto* state = ic->propertyFor(&factory_);
const bool surrvalid = ic->surroundingText().isValid();
const bool is_dbus = getFrontendName(ic) == "dbus";
state->xaChoDangCho(); // v13: chữ còn treo thì giao vào ô CŨ trước khi rời đi
state->lastDeactivateTime_ = now_ms();
if (realMode == LotusMode::Preedit && event.type() != EventType::InputContextFocusOut) {
state->commitBuffer();
Expand Down
218 changes: 212 additions & 6 deletions src/lotus-state.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,92 @@ namespace fcitx {
ic_->updateUserInterface(UserInterfaceComponent::InputPanel);
}

bool LotusState::oDaXoaXong() const {
const auto& s = ic_->surroundingText();
if (!s.isValid()) {
return false;
}
const std::string& t = s.text();
// v4: ảnh y hệt ảnh lúc bắn phím xoá thì chưa thể là "xoá xong" — ở nhịp 20 ms ảnh cũ tụt
// lại hai phím trông giống hệt trạng thái sau khi xoá (đo được: 'đươợ').
if (!cho_anh_luc_gui_.empty() && t + "\x1f" + std::to_string(s.cursor()) == cho_anh_luc_gui_) {
// v8: ở 5 ms ảnh lúc bắn LUÔN chậm một phím nên trông y hệt trạng thái xong, và Firefox không
// gửi trạng thái trung gian — đo được 37/52 lần quá hạn là tin đúng bị gạt, mỗi lần 200 ms.
// Nội dung không phân biệt được; dùng thời gian: đã chờ đủ ngưỡng Slow (8 ms × phím xoá,
// đo 60/60) thì giao không kém an toàn hơn Slow. Kiểm "ngay" (0 ms) vẫn bị chặn như v4.
const auto da_cho = (::fcitx::now(CLOCK_MONOTONIC) - cho_surr_bat_dau_) / 1000;
const auto toi_thieu = static_cast<uint64_t>(engine_->config().waitSurroundingMinPerKeyMs.value())
* static_cast<uint64_t>(std::max(expected_backspaces_, 1));
if (da_cho < toi_thieu) {
return false;
}
}
auto it = t.begin();
for (unsigned int i = 0; i < s.cursor() && it != t.end(); ++i) {
it = utf8::nextChar(it);
}
const std::string before(t.begin(), it);
auto endsWith = [](const std::string& a, const std::string& b) {
return a.size() >= b.size() && a.compare(a.size() - b.size(), b.size(), b) == 0;
};
if (!cho_deleted_.empty() && endsWith(before, cho_prefix_ + cho_deleted_)) {
return false; // ảnh cũ: phần cần xoá vẫn còn
}
return endsWith(before, cho_prefix_);
}

// v13: đường chờ sự kiện trả vòng lặp về NGAY, nên người dùng kịp đổi cửa sổ trong lúc chữ
// còn treo. deactivate() tắt is_deleting_ ⇒ đồng hồ nổ sau đó thấy cờ tắt là bỏ chữ không một
// lời (đo được: thanh địa chỉ Edge còn 't' đáng lẽ 'tô'). Đường ngủ cũ không có khe này vì nó
// ngủ ngay trong hàm xử lý phím. Giao nốt trước khi rời ô.
void LotusState::xaChoDangCho() {
if (!cho_dang_cho_) {
return;
}
ketThucThayChu("mat tieu diem", false);
}

void LotusState::ketThucThayChu(const char* ly_do, bool tu_timer) {
const auto tre_ms = (::fcitx::now(CLOCK_MONOTONIC) - cho_surr_bat_dau_) / 1000;
LOTUS_INFO("Surr wait " + std::string(ly_do) + " after " + std::to_string(tre_ms) + " ms");
if (std::string(ly_do) == "qua han") {
cho_anh_tin_cay_ = false;
++cho_qua_han_lien_tiep_;
// v11: đòi ≥ 2 tin và KHÔNG tin nào khác ảnh lúc bắn — Firefox tụt thật thường im hẳn
// (0 tin) hoặc gửi ảnh đang nhích, không tính là đóng băng.
if (cho_so_tin_ >= 2 && !cho_tin_khac_ && cho_anh_gui_cap_nhat_) {
if (++cho_dong_bang_lien_tiep_ >= 2 && !cho_dong_bang_) {
cho_dong_bang_ = true;
cho_dem_tham_do_ = 0;
LOTUS_INFO("Surr frozen: stop waiting");
}
} else {
cho_dong_bang_lien_tiep_ = 0;
}
} else if (std::string(ly_do) == "su kien" || std::string(ly_do) == "nguong") {
cho_anh_tin_cay_ = true;
cho_qua_han_lien_tiep_ = 0;
cho_dong_bang_lien_tiep_ = 0;
if (cho_dong_bang_) {
cho_dong_bang_ = false;
LOTUS_INFO("Surr frozen: resume waiting");
}
}
cho_dang_cho_ = false;
if (!tu_timer && cho_surr_timer_) {
cho_surr_timer_.reset(); // không reset từ trong chính callback của nó
}
if (!pending_commit_string_.empty()) {
ic_->commitString(pending_commit_string_);
LOTUS_INFO("Commit: " + pending_commit_string_);
}
expected_backspaces_ = 0;
current_backspace_count_ = 0;
pending_commit_string_.clear();
is_deleting_.store(false);
replayBufferedKeys();
}

bool LotusState::handleUInputKeyPress(KeyEvent& event, KeySym currentSym, int sleepTime) {
if (!is_deleting_.load()) {
return false;
Expand All @@ -445,10 +531,108 @@ namespace fcitx {
if (current_backspace_count_ < expected_backspaces_) {
return false; // Allow intermediate backspaces to reach the app to clear autofill/old text.
}
std::this_thread::sleep_for(std::chrono::milliseconds(sleepTime * (expected_backspaces_ - 1)));
// v7: app khai "có surrounding text" nhưng gửi ảnh RỖNG (Konsole: valid=1 len=0 suốt) thì
// không tin nào khớp được, chờ chỉ tốn trọn hạn mỗi dấu → đi đường ngủ cũ ở dưới.
const bool anh_rong = ic_->surroundingText().text().empty();
if (engine_->config().waitSurroundingEvent.value() && anh_rong) {
LOTUS_INFO("Surr wait skip: empty snapshot");
}
bool bo_cho_dong_bang = false;
if (engine_->config().waitSurroundingEvent.value() && !anh_rong && cho_dong_bang_) {
const int moi = std::max(engine_->config().waitSurroundingProbeEvery.value(), 1);
++cho_dem_tham_do_;
if (cho_dem_tham_do_ % moi != 0) {
bo_cho_dong_bang = true;
}
}
if (engine_->config().waitSurroundingEvent.value() && !anh_rong && !bo_cho_dong_bang) {
// fcitx5 chỉ có MỘT vòng lặp sự kiện, nên ngủ + thử lại ở dưới
// không bao giờ thấy được tin "ô đã đổi" đến trong lúc ngủ. Ở đây trả vòng lặp
// về ngay, đăng ký nghe InputContextSurroundingTextUpdated ĐÚNG LÚC NÀY (v1 để
// người nghe cũ sống qua lần thay sau → bắt tin trễ của phím trước → giao sớm →
// 0/15). Kiểm bằng NỘI DUNG (oDaXoaXong), không dùng realtextLen. Quá hạn thì
// giao như cũ. Phím người gõ trong lúc chờ vẫn vào buffered_keys_.
event.filterAndAccept();
cho_surr_bat_dau_ = ::fcitx::now(CLOCK_MONOTONIC);
// v6: sau một lần quá hạn thì Firefox đang tụt lại, ảnh không đáng tin → bỏ kiểm ngay
if (cho_anh_tin_cay_ && oDaXoaXong()) {
LOTUS_INFO("Skip retry");
ketThucThayChu("ngay", false);
return true;
}
auto* instance = engine_->instance();
cho_dang_cho_ = true;
cho_so_tin_ = 0;
cho_tin_khac_ = false;
cho_surr_watcher_.reset(); // ngoài dispatch của nó, an toàn
cho_surr_watcher_ = instance->watchEvent(EventType::InputContextSurroundingTextUpdated, EventWatcherPhase::Default, [this](Event& e) {
auto& ice = static_cast<InputContextEvent&>(e);
if (!cho_dang_cho_ || ice.inputContext() != ic_ || !is_deleting_.load()) {
return;
}
// v9: vừa quá hạn = app đang tụt, ảnh nó báo là bộ đệm cập nhật trễ ('trươnờ': tin
// 0 ms hiện 'trư' là ảnh cũ vừa bắt kịp, không phải xoá xong). Lúc đó KHÔNG tin tin nào
// tới trước ngưỡng Slow (8 ms × phím xoá).
const auto da_cho_us = ::fcitx::now(CLOCK_MONOTONIC) - cho_surr_bat_dau_;
const auto toi_thieu_us = static_cast<uint64_t>(engine_->config().waitSurroundingMinPerKeyMs.value())
* static_cast<uint64_t>(std::max(expected_backspaces_, 1)) * 1000ULL;
{
const auto& sd0 = ic_->surroundingText();
++cho_so_tin_;
if (sd0.text() + "\x1f" + std::to_string(sd0.cursor()) != cho_anh_luc_gui_) {
cho_tin_khac_ = true;
}
}
if (!cho_anh_tin_cay_ && da_cho_us < toi_thieu_us) {
// v9: vừa quá hạn xong thì tin tới sớm là bộ đệm cũ bắt kịp, không tin.
} else if (oDaXoaXong()) {
ketThucThayChu("su kien", false);
}
// Ảnh chưa khớp thì im lặng chờ tiếp: mọi thứ đáng in ở đây đều là chữ
// người dùng vừa gõ, không đưa vào nhật ký.
});
// v7: hai lần quá hạn liên tiếp mà chưa có tin khớp = app này không cập nhật ảnh trong lúc
// xoá (Edge thanh địa chỉ: ảnh chỉ đổi khi gõ chữ thường) → rút hạn xuống mức ngắn, đủ
// trên cửa sổ vượt mặt (Slow 8 ms/phím đo 60/60); có tin khớp thì trả hạn dài lại.
const int han_ms = cho_qua_han_lien_tiep_ >= 2 ? engine_->config().waitSurroundingShortMs.value()
: engine_->config().waitSurroundingTimeoutMs.value();
const auto han = static_cast<uint64_t>(han_ms) * 1000ULL;
// v7.1: accuracy 0 với sd-event = MẶC ĐỊNH 250 ms (đồng hồ được phép nổ muộn tới 250 ms):
// đo được: hạn 40 nổ ở 64, hạn 200 nổ ở 243, tệ nhất 430. Đặt 1 ms.
// v10: 16/27 lần quá hạn còn lại của v9 là tin "xong" tới TRƯỚC ngưỡng Slow rồi app im
// luôn → bị gạt và chờ trọn 200 ms. Đặt thêm một mốc đúng tại ngưỡng: kiểm lại ảnh mới nhất
// đã nhận, khớp thì giao ngay ("nguong"); chưa thì dời đồng hồ tới hạn.
const auto nguong = static_cast<uint64_t>(engine_->config().waitSurroundingMinPerKeyMs.value())
* static_cast<uint64_t>(std::max(expected_backspaces_, 1)) * 1000ULL;
const auto moc_dau = nguong < han ? cho_surr_bat_dau_ + nguong : cho_surr_bat_dau_ + han;
cho_surr_timer_ = instance->eventLoop().addTimeEvent(CLOCK_MONOTONIC, moc_dau, 1000, [this, han](EventSourceTime* t, uint64_t) {
if (!cho_dang_cho_ || !is_deleting_.load()) {
return false;
}
const auto da_cho = ::fcitx::now(CLOCK_MONOTONIC) - cho_surr_bat_dau_;
if (da_cho + 1000 < han) {
if (oDaXoaXong()) {
ketThucThayChu("nguong", true);
return false;
}
t->setTime(cho_surr_bat_dau_ + han);
t->setOneShot();
return true;
}
ketThucThayChu("qua han", true);
return false;
});
return true;
}
// v11: đóng băng → đường ngủ cũ nhưng với hằng số của Slow (8 × (N − 1), đo 60/60 trên
// Firefox), không ngủ chồng: đo được: ngủ thêm 8 × N làm N=1 mất 24 ms, N=2 mất 34 ms.
const int ngu_moi_phim = bo_cho_dong_bang ? std::max(sleepTime, engine_->config().waitSurroundingMinPerKeyMs.value()) : sleepTime;
std::this_thread::sleep_for(std::chrono::milliseconds(ngu_moi_phim * (expected_backspaces_ - 1)));
// Validate surr cursor pos should match realtextLen after all BS applied
const auto& surr = ic_->surroundingText();
if (surr.isValid() && surr.cursor() == realtextLen.load(std::memory_order_acquire)) {
if (bo_cho_dong_bang) {
LOTUS_INFO("Skip retry (frozen)"); // v12: thử lại 3 × 2 ms là vô ích khi ảnh đóng băng
} else if (surr.isValid() && surr.cursor() == realtextLen.load(std::memory_order_acquire)) {
LOTUS_INFO("Skip retry");
} else {
// Retry x3 (2 ms each), khi can (chromium,electron,...)
Expand Down Expand Up @@ -479,6 +663,26 @@ namespace fcitx {
current_backspace_count_ = 0;
pending_commit_string_ = addedPart;
expected_backspaces_ = static_cast<int>(utf8::length(deletedPart));
cho_deleted_ = deletedPart;
{ const auto& sg = ic_->surroundingText(); cho_anh_luc_gui_ = sg.isValid() ? sg.text() + "\x1f" + std::to_string(sg.cursor()) : std::string(); }
cho_prefix_ = (oldPreBuffer_.size() >= deletedPart.size()) ? oldPreBuffer_.substr(0, oldPreBuffer_.size() - deletedPart.size()) : std::string();
{
// v12: ảnh lúc bắn "cập nhật" = phần trước con trỏ kết thúc bằng prefix+deleted. Firefox tụt
// thì ảnh lúc bắn đã chậm (không thấy phần sắp xoá) → không được tính là đóng băng
// (nhịp ngẫu nhiên kích hoạt nhầm 3 lần trên Firefox).
cho_anh_gui_cap_nhat_ = false;
const auto& sg2 = ic_->surroundingText();
if (sg2.isValid()) {
const std::string& t = sg2.text();
auto it = t.begin();
for (unsigned int i = 0; i < sg2.cursor() && it != t.end(); ++i) {
it = utf8::nextChar(it);
}
const std::string truoc(t.begin(), it);
const std::string can = cho_prefix_ + cho_deleted_;
cho_anh_gui_cap_nhat_ = truoc.size() >= can.size() && truoc.compare(truoc.size() - can.size(), can.size(), can) == 0;
}
}
const auto& surrounding = ic_->surroundingText();
const std::string surrText = surrounding.text();
bool isSurrText = engine_->config().useSurroundingTextIfPossible.value() && ic_->capabilityFlags().test(CapabilityFlag::SurroundingText) && surrounding.isValid() &&
Expand All @@ -499,11 +703,11 @@ namespace fcitx {
if (isSurrText) {
ic_->deleteSurroundingText(-expected_backspaces_, expected_backspaces_);
LOTUS_INFO("Delete using surrounding text");
std::this_thread::sleep_for(std::chrono::milliseconds(4 * expected_backspaces_));
std::this_thread::sleep_for(std::chrono::milliseconds(engine_->config().surrDeleteSleepMs.value() * expected_backspaces_));
if (!pending_commit_string_.empty()) {
ic_->commitString(pending_commit_string_);
LOTUS_INFO("Commit: " + pending_commit_string_);
std::this_thread::sleep_for(std::chrono::milliseconds(3 * utf8::length(addedPart)));
std::this_thread::sleep_for(std::chrono::milliseconds(engine_->config().surrCommitSleepMs.value() * utf8::length(addedPart)));
}
expected_backspaces_ = 0;
current_backspace_count_ = 0;
Expand Down Expand Up @@ -807,7 +1011,7 @@ namespace fcitx {

if (charsToDelete > 0) {
ic->deleteSurroundingText(-static_cast<int>(charsToDelete), static_cast<int>(charsToDelete));
std::this_thread::sleep_for(std::chrono::milliseconds(4 * charsToDelete));
std::this_thread::sleep_for(std::chrono::milliseconds(engine_->config().surrDeleteSleepMs.value() * charsToDelete));
}

if (!addedPart.empty()) {
Expand Down Expand Up @@ -1004,7 +1208,9 @@ namespace fcitx {
LOTUS_WARN("Cannot connect to uinput server, reconnecting....");
connect_uinput_server();
}
if (current_backspace_count_ >= expected_backspaces_ && is_deleting_.load()) {
// Van an toàn này tắt cờ lặng lẽ ở phím kế tiếp; khi đang chờ sự kiện thì phải bỏ qua,
// nếu không chữ chờ giao bị vứt (v2: " Nam").
if (!cho_dang_cho_ && current_backspace_count_ >= expected_backspaces_ && is_deleting_.load()) {
is_deleting_.store(false);
current_backspace_count_ = 0;
expected_backspaces_ = 0;
Expand Down
Loading
Loading