mirror of
https://github.com/Melorise/TamaASR.git
synced 2026-11-07 14:39:32 +00:00
feat: 实现 Linux Fcitx5 实时输入链路
加入 Unix Socket v2 协议、会话状态机、动态快捷键、完整 preedit 更新、结束提交以及断线重连和安全取消。 AI-assisted-by: Codex AI-model: gpt-5.6-sol AI-reasoning-effort: medium Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
project(meloasr-fcitx5 VERSION 0.1.0 LANGUAGES CXX)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
include(CTest)
|
||||
find_package(Fcitx5Core 5.0 REQUIRED)
|
||||
find_package(Fcitx5Utils 5.0 REQUIRED)
|
||||
find_package(nlohmann_json 3.2 REQUIRED)
|
||||
|
||||
add_library(meloasr MODULE voiceinput.cpp)
|
||||
target_compile_features(meloasr PRIVATE cxx_std_20)
|
||||
target_compile_options(meloasr PRIVATE -Wall -Wextra -Wpedantic)
|
||||
target_link_libraries(meloasr PRIVATE Fcitx5::Core Fcitx5::Utils nlohmann_json::nlohmann_json)
|
||||
set_target_properties(meloasr PROPERTIES PREFIX "lib")
|
||||
|
||||
set(MELOASR_LIBRARY_PATH "${FCITX_INSTALL_ADDONDIR}/libmeloasr")
|
||||
configure_file(meloasr.conf.in meloasr.conf @ONLY)
|
||||
install(TARGETS meloasr LIBRARY DESTINATION "${FCITX_INSTALL_ADDONDIR}")
|
||||
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/meloasr.conf" DESTINATION "${FCITX_INSTALL_PKGDATADIR}/addon")
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_executable(protocolsession_test protocolsession_test.cpp)
|
||||
target_compile_features(protocolsession_test PRIVATE cxx_std_17)
|
||||
add_test(NAME protocol-session COMMAND protocolsession_test)
|
||||
endif()
|
||||
@@ -0,0 +1,25 @@
|
||||
# MeloASR Fcitx5 addon
|
||||
|
||||
该 addon 作为 Fcitx5 常驻 Module 工作,不需要切换到独立输入法。
|
||||
|
||||
- 按住设置的快捷键:向 Electron 请求开始语音输入;
|
||||
- 松开主键:请求停止;
|
||||
- `Esc`、普通键盘输入、焦点丢失、InputContext reset 或 Socket 断开:取消且不提交;
|
||||
- Electron 每次发送网页编辑器的完整文本时,addon 整体替换 client preedit;
|
||||
- `finish` 只调用一次 `commitString()`。
|
||||
|
||||
用户级安装:
|
||||
|
||||
```bash
|
||||
./linux/fcitx5/install-user.sh
|
||||
fcitx5 -r
|
||||
```
|
||||
|
||||
安装文件:
|
||||
|
||||
```text
|
||||
~/.local/lib/fcitx5/libmeloasr.so
|
||||
~/.local/share/fcitx5/addon/meloasr.conf
|
||||
```
|
||||
|
||||
本地协议使用 `$XDG_RUNTIME_DIR/meloasr/fcitx5.sock`,目录权限 `0700`,Socket 权限 `0600`。协议版本为 2,支持 `configure` 动态快捷键和 `activate` 悬浮球启动请求。
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
project_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
build_dir="${project_dir}/build"
|
||||
|
||||
cmake -S "${project_dir}" -B "${build_dir}" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX="${HOME}/.local" \
|
||||
-DCMAKE_INSTALL_LIBDIR=lib \
|
||||
-DCMAKE_INSTALL_DATADIR=share
|
||||
cmake --build "${build_dir}" --parallel
|
||||
cmake --install "${build_dir}"
|
||||
|
||||
# 0.1.0 之前的原型使用 voiceinput 名称;升级时移除,避免两个常驻模块同时监听快捷键。
|
||||
rm -f -- \
|
||||
"${HOME}/.local/lib/fcitx5/libvoiceinput.so" \
|
||||
"${HOME}/.local/share/fcitx5/addon/voiceinput.conf"
|
||||
|
||||
echo "已安装 MeloASR Fcitx5 插件,请重启 Fcitx5。"
|
||||
@@ -0,0 +1,14 @@
|
||||
[Addon]
|
||||
Name=MeloASR
|
||||
Name[zh_CN]=MeloASR
|
||||
Comment=Connect MeloASR web backends to the active input context
|
||||
Comment[zh_CN]=将 MeloASR 网页后端连接到当前输入上下文
|
||||
Type=SharedLibrary
|
||||
Library=@MELOASR_LIBRARY_PATH@
|
||||
Category=Module
|
||||
Version=0.1.0
|
||||
OnDemand=False
|
||||
Configurable=False
|
||||
|
||||
[Addon/Dependencies]
|
||||
0=core:5.0.0
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
class ProtocolSession {
|
||||
public:
|
||||
void start(std::string sessionId) {
|
||||
sessionId_ = std::move(sessionId);
|
||||
revision_ = 0;
|
||||
}
|
||||
|
||||
bool active() const { return !sessionId_.empty(); }
|
||||
|
||||
bool accept(const std::string &sessionId, std::int64_t revision) {
|
||||
if (!active() || sessionId != sessionId_ || revision <= revision_) return false;
|
||||
revision_ = revision;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool matches(const std::string &sessionId) const {
|
||||
return active() && sessionId == sessionId_;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
sessionId_.clear();
|
||||
revision_ = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string sessionId_;
|
||||
std::int64_t revision_ = 0;
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
#include "protocolsession.h"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
int main() {
|
||||
ProtocolSession session;
|
||||
assert(!session.active());
|
||||
session.start("round-1");
|
||||
assert(session.active());
|
||||
assert(session.accept("round-1", 1));
|
||||
assert(!session.accept("round-1", 1));
|
||||
assert(!session.accept("round-1", 0));
|
||||
assert(!session.accept("old-round", 2));
|
||||
assert(session.accept("round-1", 3));
|
||||
session.reset();
|
||||
assert(!session.active());
|
||||
assert(!session.accept("round-1", 4));
|
||||
session.start("round-2");
|
||||
assert(!session.accept("round-1", 100));
|
||||
assert(session.accept("round-2", 1));
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <fcitx-utils/event.h>
|
||||
#include <fcitx-utils/eventloopinterface.h>
|
||||
#include <fcitx-utils/capabilityflags.h>
|
||||
#include <fcitx-utils/key.h>
|
||||
#include <fcitx-utils/keysym.h>
|
||||
#include <fcitx-utils/log.h>
|
||||
#include <fcitx/addonfactory.h>
|
||||
#include <fcitx/addoninstance.h>
|
||||
#include <fcitx/addonmanager.h>
|
||||
#include <fcitx/event.h>
|
||||
#include <fcitx/inputcontext.h>
|
||||
#include <fcitx/inputpanel.h>
|
||||
#include <fcitx/instance.h>
|
||||
#include <fcitx/text.h>
|
||||
|
||||
#include "protocolsession.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kProtocolVersion = 2;
|
||||
constexpr std::size_t kMaxBufferBytes = 1024 * 1024;
|
||||
|
||||
class VoiceInputAddon final : public fcitx::AddonInstance {
|
||||
public:
|
||||
explicit VoiceInputAddon(fcitx::Instance *instance)
|
||||
: instance_(instance), trigger_("Control+Shift+space") {
|
||||
keyWatcher_ = instance_->watchEvent(
|
||||
fcitx::EventType::InputContextKeyEvent,
|
||||
// 必须在原输入法之前吞掉快捷键;否则长按产生的重复事件会先触发
|
||||
// Fcitx5 自身的输入法切换窗口,并打断网页录音焦点。
|
||||
fcitx::EventWatcherPhase::PreInputMethod,
|
||||
[this](fcitx::Event &event) {
|
||||
handleKeyEvent(static_cast<fcitx::KeyEvent &>(event));
|
||||
});
|
||||
focusOutWatcher_ = instance_->watchEvent(
|
||||
fcitx::EventType::InputContextFocusOut,
|
||||
fcitx::EventWatcherPhase::PostInputMethod,
|
||||
[this](fcitx::Event &event) {
|
||||
cancelForContext(static_cast<fcitx::InputContextEvent &>(event).inputContext());
|
||||
});
|
||||
focusInWatcher_ = instance_->watchEvent(
|
||||
fcitx::EventType::InputContextFocusIn,
|
||||
fcitx::EventWatcherPhase::PostInputMethod,
|
||||
[this](fcitx::Event &event) {
|
||||
focusedInputContext_ =
|
||||
static_cast<fcitx::InputContextEvent &>(event).inputContext();
|
||||
});
|
||||
resetWatcher_ = instance_->watchEvent(
|
||||
fcitx::EventType::InputContextReset,
|
||||
fcitx::EventWatcherPhase::PostInputMethod,
|
||||
[this](fcitx::Event &event) {
|
||||
cancelForContext(static_cast<fcitx::InputContextEvent &>(event).inputContext());
|
||||
});
|
||||
destroyedWatcher_ = instance_->watchEvent(
|
||||
fcitx::EventType::InputContextDestroyed,
|
||||
fcitx::EventWatcherPhase::PostInputMethod,
|
||||
[this](fcitx::Event &event) {
|
||||
auto *context = static_cast<fcitx::InputContextEvent &>(event).inputContext();
|
||||
if (context == focusedInputContext_) focusedInputContext_ = nullptr;
|
||||
if (context == inputContext_) {
|
||||
sendControl("request-cancel");
|
||||
resetLocalState(false, false);
|
||||
}
|
||||
});
|
||||
reconnectTimer_ = instance_->eventLoop().addTimeEvent(
|
||||
CLOCK_MONOTONIC, fcitx::now(CLOCK_MONOTONIC) + 100000, 100000,
|
||||
[this](fcitx::EventSourceTime *source, uint64_t) {
|
||||
if (socketFd_ < 0) ensureConnected();
|
||||
source->setNextInterval(1000000);
|
||||
// 时间事件触发后会退出 one-shot 状态;重新启用才能持续探测
|
||||
// Electron 的重启和冷启动。
|
||||
source->setOneShot();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
~VoiceInputAddon() override {
|
||||
if (inputContext_) {
|
||||
clearPreedit();
|
||||
restorePreeditState();
|
||||
}
|
||||
disconnectSocket();
|
||||
}
|
||||
|
||||
private:
|
||||
void handleKeyEvent(fcitx::KeyEvent &event) {
|
||||
const bool isTriggerPress = !event.isRelease() && event.key().check(trigger_);
|
||||
const bool isTriggerRelease = event.isRelease() && held_ &&
|
||||
event.rawKey().sym() == trigger_.sym();
|
||||
|
||||
if (isTriggerPress) {
|
||||
event.filterAndAccept();
|
||||
if (event.rawKey().states().test(fcitx::KeyState::Repeat)) return;
|
||||
FCITX_INFO() << "网页语音输入:快捷键按下 held=" << held_
|
||||
<< " awaiting=" << awaitingStart_
|
||||
<< " active=" << session_.active();
|
||||
if (held_) return;
|
||||
if (!ensureConnected()) {
|
||||
FCITX_WARN() << "网页语音输入:Electron Socket 尚未就绪";
|
||||
return;
|
||||
}
|
||||
held_ = true;
|
||||
if (!prepareInputContext(event.inputContext())) held_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isTriggerRelease) {
|
||||
event.filterAndAccept();
|
||||
FCITX_INFO() << "网页语音输入:快捷键松开 awaiting=" << awaitingStart_
|
||||
<< " active=" << session_.active();
|
||||
held_ = false;
|
||||
if (awaitingStart_ || session_.active()) sendControl("request-stop");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!session_.active() && !awaitingStart_) return;
|
||||
if (event.isRelease() || event.key().isModifier()) return;
|
||||
|
||||
if (event.key().check(FcitxKey_Escape)) {
|
||||
event.filterAndAccept();
|
||||
sendControl("request-cancel");
|
||||
clearPreedit();
|
||||
resetLocalState(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 用户开始键盘输入时,立即取消语音会话,并让该按键继续交给原输入法。
|
||||
sendControl("request-cancel");
|
||||
clearPreedit();
|
||||
resetLocalState(false);
|
||||
}
|
||||
|
||||
bool prepareInputContext(fcitx::InputContext *context) {
|
||||
if (!context || !context->hasFocus()) return false;
|
||||
if (!context->inputPanel().clientPreedit().empty() ||
|
||||
!context->inputPanel().preedit().empty()) {
|
||||
FCITX_WARN() << "MeloASR:当前输入法存在未提交文本,本轮未启动";
|
||||
return false;
|
||||
}
|
||||
inputContext_ = context;
|
||||
preeditWasEnabled_ = inputContext_->isPreeditEnabled();
|
||||
preeditStateCaptured_ = true;
|
||||
inputContext_->setEnablePreedit(true);
|
||||
awaitingStart_ = true;
|
||||
sendControl("request-start");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ensureConnected() {
|
||||
if (socketFd_ >= 0) {
|
||||
char byte;
|
||||
const ssize_t probe = ::recv(socketFd_, &byte, sizeof(byte), MSG_PEEK | MSG_DONTWAIT);
|
||||
if (probe > 0 || (probe < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))) {
|
||||
return true;
|
||||
}
|
||||
FCITX_INFO() << "网页语音输入:检测到失效的 Electron Socket,立即重连";
|
||||
handleDisconnect();
|
||||
}
|
||||
const char *runtimeDir = std::getenv("XDG_RUNTIME_DIR");
|
||||
if (!runtimeDir || !*runtimeDir) return false;
|
||||
const std::string socketPath = std::string(runtimeDir) + "/meloasr/fcitx5.sock";
|
||||
const int fd = ::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
|
||||
if (fd < 0) return false;
|
||||
sockaddr_un address{};
|
||||
if (socketPath.size() >= sizeof(address.sun_path)) {
|
||||
::close(fd);
|
||||
return false;
|
||||
}
|
||||
address.sun_family = AF_UNIX;
|
||||
std::memcpy(address.sun_path, socketPath.c_str(), socketPath.size() + 1);
|
||||
if (::connect(fd, reinterpret_cast<sockaddr *>(&address), sizeof(address)) < 0) {
|
||||
::close(fd);
|
||||
return false;
|
||||
}
|
||||
const int flags = ::fcntl(fd, F_GETFL, 0);
|
||||
if (flags >= 0) ::fcntl(fd, F_SETFL, flags | O_NONBLOCK);
|
||||
socketFd_ = fd;
|
||||
socketWatcher_ = instance_->eventLoop().addIOEvent(
|
||||
socketFd_, fcitx::IOEventFlags{fcitx::IOEventFlag::In, fcitx::IOEventFlag::Err,
|
||||
fcitx::IOEventFlag::Hup},
|
||||
[this](fcitx::EventSourceIO *, int, fcitx::IOEventFlags flags) {
|
||||
if (flags.test(fcitx::IOEventFlag::Err) || flags.test(fcitx::IOEventFlag::Hup)) {
|
||||
handleDisconnect();
|
||||
return false;
|
||||
}
|
||||
return readMessages();
|
||||
});
|
||||
return writeJson({{"type", "hello"}, {"protocol", kProtocolVersion}});
|
||||
}
|
||||
|
||||
bool readMessages() {
|
||||
char chunk[8192];
|
||||
while (true) {
|
||||
const ssize_t size = ::recv(socketFd_, chunk, sizeof(chunk), 0);
|
||||
if (size > 0) {
|
||||
readBuffer_.append(chunk, static_cast<std::size_t>(size));
|
||||
if (readBuffer_.size() > kMaxBufferBytes) {
|
||||
handleDisconnect();
|
||||
return false;
|
||||
}
|
||||
consumeLines();
|
||||
continue;
|
||||
}
|
||||
if (size == 0) {
|
||||
handleDisconnect();
|
||||
return false;
|
||||
}
|
||||
if (errno == EINTR) continue;
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) return true;
|
||||
handleDisconnect();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void consumeLines() {
|
||||
std::size_t newline;
|
||||
while ((newline = readBuffer_.find('\n')) != std::string::npos) {
|
||||
std::string line = readBuffer_.substr(0, newline);
|
||||
readBuffer_.erase(0, newline + 1);
|
||||
if (line.empty()) continue;
|
||||
try {
|
||||
handleMessage(nlohmann::json::parse(line));
|
||||
} catch (const std::exception &error) {
|
||||
FCITX_WARN() << "网页语音输入协议解析失败:" << error.what();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void handleMessage(const nlohmann::json &message) {
|
||||
if (!message.is_object() || !message.contains("type") || !message["type"].is_string()) return;
|
||||
const std::string type = message["type"].get<std::string>();
|
||||
if (type == "hello") {
|
||||
if (message.value("protocol", 0) != kProtocolVersion) handleDisconnect();
|
||||
return;
|
||||
}
|
||||
if (type == "configure") {
|
||||
if (!message.contains("shortcut") || !message["shortcut"].is_string() ||
|
||||
awaitingStart_ || session_.active()) return;
|
||||
fcitx::Key candidate(message["shortcut"].get<std::string>());
|
||||
if (!candidate.isValid() || !candidate.hasModifier()) {
|
||||
FCITX_WARN() << "MeloASR:拒绝无效快捷键";
|
||||
return;
|
||||
}
|
||||
trigger_ = std::move(candidate);
|
||||
FCITX_INFO() << "MeloASR:快捷键已更新为 " << trigger_.toString();
|
||||
return;
|
||||
}
|
||||
if (type == "activate") {
|
||||
if (!awaitingStart_ && !session_.active()) {
|
||||
prepareInputContext(focusedInputContext_);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (type == "start") {
|
||||
if (!awaitingStart_ || !inputContext_ || !inputContext_->hasFocus()) return;
|
||||
if (!message.contains("sessionId") || !message["sessionId"].is_string()) return;
|
||||
session_.start(message["sessionId"].get<std::string>());
|
||||
awaitingStart_ = false;
|
||||
FCITX_INFO() << "网页语音输入:会话开始,clientPreedit="
|
||||
<< inputContext_->capabilityFlags().test(fcitx::CapabilityFlag::Preedit);
|
||||
setPreedit("");
|
||||
return;
|
||||
}
|
||||
if (type == "cancel") {
|
||||
const std::string incoming = message.value("sessionId", std::string());
|
||||
if (!incoming.empty() && !session_.matches(incoming)) return;
|
||||
clearPreedit();
|
||||
resetLocalState(false);
|
||||
return;
|
||||
}
|
||||
if (type == "update" || type == "finish") {
|
||||
if (!message.contains("revision") || !message["revision"].is_number_integer() ||
|
||||
!message.contains("text") || !message["text"].is_string()) return;
|
||||
const std::int64_t incomingRevision = message["revision"].get<std::int64_t>();
|
||||
if (!session_.accept(message.value("sessionId", std::string()), incomingRevision)) return;
|
||||
currentText_ = message["text"].get<std::string>();
|
||||
FCITX_INFO() << "网页语音输入:收到 " << type << " revision="
|
||||
<< incomingRevision << " bytes=" << currentText_.size();
|
||||
setPreedit(currentText_);
|
||||
if (type == "finish") {
|
||||
if (inputContext_ && inputContext_->hasFocus() && !currentText_.empty()) {
|
||||
inputContext_->commitString(currentText_);
|
||||
}
|
||||
clearPreedit();
|
||||
resetLocalState(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setPreedit(const std::string &text) {
|
||||
if (!inputContext_ || !inputContext_->hasFocus()) return;
|
||||
fcitx::Text formatted(text);
|
||||
formatted.setCursor(static_cast<int>(text.size()));
|
||||
inputContext_->inputPanel().setClientPreedit(formatted);
|
||||
inputContext_->updatePreedit();
|
||||
}
|
||||
|
||||
void clearPreedit() {
|
||||
if (!inputContext_) return;
|
||||
inputContext_->inputPanel().setClientPreedit(fcitx::Text());
|
||||
if (inputContext_->hasFocus()) {
|
||||
inputContext_->updatePreedit();
|
||||
}
|
||||
}
|
||||
|
||||
void cancelForContext(fcitx::InputContext *context) {
|
||||
if (context == focusedInputContext_) focusedInputContext_ = nullptr;
|
||||
if (context != inputContext_) return;
|
||||
sendControl("request-cancel");
|
||||
clearPreedit();
|
||||
resetLocalState(false);
|
||||
}
|
||||
|
||||
void restorePreeditState() {
|
||||
if (inputContext_ && preeditStateCaptured_) {
|
||||
inputContext_->setEnablePreedit(preeditWasEnabled_);
|
||||
}
|
||||
preeditStateCaptured_ = false;
|
||||
}
|
||||
|
||||
void resetLocalState(bool keepContext, bool restorePreedit = true) {
|
||||
if (restorePreedit) restorePreeditState();
|
||||
held_ = false;
|
||||
awaitingStart_ = false;
|
||||
session_.reset();
|
||||
currentText_.clear();
|
||||
if (!keepContext) inputContext_ = nullptr;
|
||||
}
|
||||
|
||||
bool sendControl(const char *type) { return writeJson({{"type", type}}); }
|
||||
|
||||
bool writeJson(const nlohmann::json &message) {
|
||||
if (socketFd_ < 0) return false;
|
||||
const std::string line = message.dump() + "\n";
|
||||
std::size_t offset = 0;
|
||||
while (offset < line.size()) {
|
||||
const ssize_t written = ::send(socketFd_, line.data() + offset, line.size() - offset, MSG_NOSIGNAL);
|
||||
if (written > 0) {
|
||||
offset += static_cast<std::size_t>(written);
|
||||
continue;
|
||||
}
|
||||
if (written < 0 && errno == EINTR) continue;
|
||||
handleDisconnect();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void handleDisconnect() {
|
||||
clearPreedit();
|
||||
resetLocalState(false);
|
||||
disconnectSocket();
|
||||
}
|
||||
|
||||
void disconnectSocket() {
|
||||
socketWatcher_.reset();
|
||||
if (socketFd_ >= 0) ::close(socketFd_);
|
||||
socketFd_ = -1;
|
||||
readBuffer_.clear();
|
||||
}
|
||||
|
||||
fcitx::Instance *instance_;
|
||||
fcitx::Key trigger_;
|
||||
fcitx::InputContext *inputContext_ = nullptr;
|
||||
fcitx::InputContext *focusedInputContext_ = nullptr;
|
||||
bool held_ = false;
|
||||
bool awaitingStart_ = false;
|
||||
bool preeditStateCaptured_ = false;
|
||||
bool preeditWasEnabled_ = true;
|
||||
ProtocolSession session_;
|
||||
std::string currentText_;
|
||||
int socketFd_ = -1;
|
||||
std::string readBuffer_;
|
||||
std::unique_ptr<fcitx::EventSourceIO> socketWatcher_;
|
||||
std::unique_ptr<fcitx::EventSourceTime> reconnectTimer_;
|
||||
std::unique_ptr<fcitx::HandlerTableEntry<fcitx::EventHandler>> keyWatcher_;
|
||||
std::unique_ptr<fcitx::HandlerTableEntry<fcitx::EventHandler>> focusOutWatcher_;
|
||||
std::unique_ptr<fcitx::HandlerTableEntry<fcitx::EventHandler>> focusInWatcher_;
|
||||
std::unique_ptr<fcitx::HandlerTableEntry<fcitx::EventHandler>> resetWatcher_;
|
||||
std::unique_ptr<fcitx::HandlerTableEntry<fcitx::EventHandler>> destroyedWatcher_;
|
||||
};
|
||||
|
||||
class VoiceInputAddonFactory final : public fcitx::AddonFactory {
|
||||
public:
|
||||
fcitx::AddonInstance *create(fcitx::AddonManager *manager) override {
|
||||
return new VoiceInputAddon(manager->instance());
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
FCITX_ADDON_FACTORY(VoiceInputAddonFactory);
|
||||
Reference in New Issue
Block a user