diff --git a/backends.json b/backends.json index ca649fc0c..8b75f8b28 100644 --- a/backends.json +++ b/backends.json @@ -10,11 +10,5 @@ "command": "node\\node.exe", "workingDir": "node", "params": "server.js" - }, - { - "name": "go-backend", - "command": "go-backend\\server.exe", - "workingDir": "go-backend", - "params": "" } ] diff --git a/go-backend/README.md b/go-backend/README.md deleted file mode 100644 index 9e62033e6..000000000 --- a/go-backend/README.md +++ /dev/null @@ -1,179 +0,0 @@ -# PIME Go 后端 - -使用 Go 语言实现的 PIME 输入法后端框架。 - -## 项目结构 - -``` -go-backend/ -├── pime/ # PIME 核心库 -│ ├── protocol.go # 通信协议定义 -│ ├── server.go # 服务器实现 -│ ├── service.go # 文本服务接口 -│ └── service_manager.go # 服务管理器 -├── input_methods/ # 示例实现 -│ └── simple_ime/ # 简单输入法示例 -│ ├── main.go # 入口文件 -│ └── ime.go # 输入法实现 -├── go.mod # Go 模块定义 -└── README.md # 说明文档 -``` - -## 快速开始 - -### 1. 编译 - -```bash -cd go-backend -build.bat -``` - -`build.bat` 会生成可直接安装的运行目录: - -```text -build/ -├── backends.go-backend.json -└── go-backend/ - ├── server.exe - └── input_methods/ -``` - -### 2. 配置 PIME - -在 PIME 根目录的 `backends.json` 中添加 Go 后端配置。 - -注意:这个仓库里的 `backends.json` 顶层是数组,不是 `{ "backends": [...] }`。 - -```json -[ - { - "name": "go-backend", - "command": "go-backend\\server.exe", - "workingDir": "go-backend", - "params": "" - } -] -``` - -### 3. 注册输入法 - -确保 `C:\Program Files (x86)\PIME\go-backend\input_methods\*\ime.json` 存在。比如: - -```json -{ - "name": "GoSimpleIME", - "icon": "icon.ico", - "backend": "go-backend" -} -``` - -## 开发自定义输入法 - -### 1. 实现 TextService 接口 - -```go -type MyIME struct { - *pime.TextServiceBase - // 自定义字段 -} - -func NewMyIME(client *pime.Client) *MyIME { - return &MyIME{ - TextServiceBase: pime.NewTextServiceBase(client), - } -} - -func (ime *MyIME) HandleRequest(req *pime.Request) *pime.Response { - resp := pime.NewResponse(req.SeqNum, true) - - switch req.Method { - case "filterKeyDown": - // 处理按键 - return ime.handleKeyDown(req, resp) - // ... 其他方法 - } - - return resp -} -``` - -### 2. 注册到服务管理器 - -```go -func main() { - mgr := pime.NewServiceManager() - - // 注册输入法 - mgr.Register("my_ime", func(clientID string) pime.TextService { - return NewMyIME(&pime.Client{ID: clientID}) - }) - - // 运行服务 - if err := mgr.Run(); err != nil { - log.Fatal(err) - } -} -``` - -## 协议说明 - -### 通信方式 - -- 使用 stdin/stdout 进行通信 -- 每行一条消息 -- JSON 格式 - -### 请求格式 - -``` -| -``` - -### 响应格式 - -``` -PIME_MSG|| -``` - -### 消息类型 - -#### 初始化 -```json -{ - "method": "init", - "id": "client_guid", - "isWindows8Above": true, - "isMetroApp": false, - "isUiLess": false, - "isConsole": false -} -``` - -#### 按键处理 -```json -{ - "method": "filterKeyDown", - "keyCode": 65, - "charCode": 97, - "scanCode": 30 -} -``` - -#### 响应 -```json -{ - "success": true, - "returnValue": 1, - "compositionString": "a", - "candidateList": ["啊", "阿", "吖"], - "showCandidates": true -} -``` - -## 贡献 - -欢迎提交 Issue 和 Pull Request! - -## 许可证 - -LGPL-2.1 License diff --git a/go-backend/build.bat b/go-backend/build.bat deleted file mode 100644 index 8bb5e52fe..000000000 --- a/go-backend/build.bat +++ /dev/null @@ -1,369 +0,0 @@ -@echo off -setlocal - -echo ============================================ -echo PIME Go Backend Build Script -echo ============================================ -echo. - -set "ROOT_DIR=%~dp0" -if "%ROOT_DIR:~-1%"=="\" set "ROOT_DIR=%ROOT_DIR:~0,-1%" -for %%I in ("%ROOT_DIR%\..") do set "PIME_ROOT=%%~fI" -set "BUILD_ROOT=%ROOT_DIR%\build" -set "PACKAGE_DIR=%BUILD_ROOT%\go-backend" -set "SERVER_EXE=%PACKAGE_DIR%\server.exe" -set "BACKEND_SNIPPET=%BUILD_ROOT%\backends.go-backend.json" -set "RIME_DIR=%ROOT_DIR%\input_methods\rime" -set "RIME_DATA_DIR=%RIME_DIR%\data" -set "PACKAGE_RIME_DIR=%PACKAGE_DIR%\input_methods\rime" -set "PACKAGE_RIME_DATA_DIR=%PACKAGE_RIME_DIR%\data" -set "PLUM_DIR=%PIME_ROOT%\plum" -set "PLUM_INSTALL=%PLUM_DIR%\rime-install" -set "PLUM_INSTALL_BAT=%PLUM_DIR%\rime-install.bat" -set "PLUM_PRESET_TARGET=:preset" -set "WEASEL_ROOT=" -set "WEASEL_DATA_DIR=" - -REM Check Go environment -where go >nul 2>nul -if errorlevel 1 ( - echo [ERROR] Go was not found in PATH. - echo Install Go from: https://golang.org/dl/ - exit /b 1 -) - -for /f "tokens=3" %%i in ('go version') do ( - echo [INFO] Go version: %%i -) - -echo. -echo ============================================ -echo Step 1: Prepare output directory -echo ============================================ -echo. - -if exist "%PACKAGE_DIR%" ( - echo [INFO] Removing old build output: "%PACKAGE_DIR%" - rmdir /s /q "%PACKAGE_DIR%" -) - -mkdir "%PACKAGE_DIR%" -if errorlevel 1 ( - echo [ERROR] Failed to create output directory: "%PACKAGE_DIR%" - exit /b 1 -) - -echo [INFO] Output directory: "%PACKAGE_DIR%" - -if not exist "%RIME_DATA_DIR%\default.yaml" ( - echo [ERROR] Missing Go Rime shared data: "%RIME_DATA_DIR%" - echo [ERROR] Initialize the submodule from https://github.com/gaboolic/rime-frost - exit /b 1 -) - -echo. -echo ============================================ -echo Step 2: Sync Go dependencies -echo ============================================ -echo. - -pushd "%ROOT_DIR%" -go mod tidy -if errorlevel 1 ( - echo [WARN] go mod tidy failed, continuing... -) - -echo. -echo ============================================ -echo Step 3: Build go-backend server -echo ============================================ -echo. - -set "GOOS=windows" -set "GOARCH=amd64" -set "CGO_ENABLED=0" - -echo [INFO] Building server.exe with dynamic DLL loading ... -go build -ldflags "-s -w" -o "%SERVER_EXE%" . -if errorlevel 1 ( - echo [ERROR] Failed to build server.exe - popd - exit /b 1 -) - -echo [INFO] Built: "%SERVER_EXE%" - -echo. -echo ============================================ -echo Step 4: Copy input_methods -echo ============================================ -echo. - -if not exist "%ROOT_DIR%\input_methods" ( - echo [ERROR] Missing input_methods directory: "%ROOT_DIR%\input_methods" - popd - exit /b 1 -) - -xcopy "%ROOT_DIR%\input_methods" "%PACKAGE_DIR%\input_methods\" /E /I /Y >nul -if errorlevel 1 ( - echo [ERROR] Failed to copy input_methods - popd - exit /b 1 -) - -echo [INFO] input_methods copied - -echo. -echo ============================================ -echo Step 5: Prepare packaged Rime shared data -echo ============================================ -echo. - -call :prepare_rime_data -if errorlevel 1 ( - echo [ERROR] Failed to prepare packaged Rime shared data - popd - exit /b 1 -) - -if exist "%PACKAGE_DIR%\input_methods\rime\brise" ( - rmdir /s /q "%PACKAGE_DIR%\input_methods\rime\brise" - if errorlevel 1 ( - echo [ERROR] Failed to remove packaged rime\brise directory - popd - exit /b 1 - ) - echo [INFO] Removed rime\brise from package output -) - -if exist "%PACKAGE_DIR%\input_methods\rime\*.go" ( - del /q "%PACKAGE_DIR%\input_methods\rime\*.go" >nul - if errorlevel 1 ( - echo [ERROR] Failed to remove packaged Go source files - popd - exit /b 1 - ) - echo [INFO] Removed packaged Go source files -) - -if exist "%PACKAGE_DIR%\input_methods\rime\rime.dll.bak-32bit" ( - del /q "%PACKAGE_DIR%\input_methods\rime\rime.dll.bak-32bit" >nul - if errorlevel 1 ( - echo [ERROR] Failed to remove packaged backup DLL - popd - exit /b 1 - ) - echo [INFO] Removed packaged backup DLL -) - -if exist "%PACKAGE_DIR%\input_methods\rime\icons\icons" ( - rmdir /s /q "%PACKAGE_DIR%\input_methods\rime\icons\icons" - if errorlevel 1 ( - echo [ERROR] Failed to remove nested icons directory - popd - exit /b 1 - ) - echo [INFO] Removed nested icons directory -) - -if exist "%RIME_DIR%\rime.dll" ( - copy /Y "%RIME_DIR%\rime.dll" "%PACKAGE_DIR%\input_methods\rime\rime.dll" >nul - echo [INFO] Copied rime.dll into package output -) - -echo. -echo ============================================ -echo Step 6: Generate backends.json snippet -echo ============================================ -echo. - -> "%BACKEND_SNIPPET%" echo [ ->> "%BACKEND_SNIPPET%" echo { ->> "%BACKEND_SNIPPET%" echo "name": "go-backend", ->> "%BACKEND_SNIPPET%" echo "command": "go-backend\\server.exe", ->> "%BACKEND_SNIPPET%" echo "workingDir": "go-backend", ->> "%BACKEND_SNIPPET%" echo "params": "" ->> "%BACKEND_SNIPPET%" echo } ->> "%BACKEND_SNIPPET%" echo ] - -echo [INFO] Generated: "%BACKEND_SNIPPET%" -popd - -echo. -echo ============================================ -echo Build completed -echo ============================================ -echo. -echo Output directory: -echo "%PACKAGE_DIR%" -echo. -echo Install target: -echo C:\Program Files (x86)\PIME\go-backend -echo. -echo Notes: -echo 1. backends.json in this repo uses a top-level array. -echo 2. Ensure C:\Program Files (x86)\PIME\backends.json includes go-backend. -echo 3. Ensure C:\Program Files (x86)\PIME\go-backend\input_methods\*\ime.json exists. -echo 4. Re-register both PIMETextService.dll files after copying. -echo 5. Ensure C:\Program Files (x86)\PIME\go-backend\input_methods\rime contains rime.dll. -echo 6. Start or restart PIMELauncher.exe after install. -echo. -exit /b 0 - -:prepare_rime_data -if exist "%PACKAGE_RIME_DATA_DIR%" ( - rmdir /s /q "%PACKAGE_RIME_DATA_DIR%" -) -mkdir "%PACKAGE_RIME_DATA_DIR%" -if errorlevel 1 ( - echo [ERROR] Failed to create packaged Rime data directory: "%PACKAGE_RIME_DATA_DIR%" - exit /b 1 -) - -echo [INFO] Copying bundled rime-frost submodule data ... -xcopy "%RIME_DATA_DIR%" "%PACKAGE_RIME_DATA_DIR%\" /E /I /Y >nul -if errorlevel 1 ( - echo [ERROR] Failed to copy bundled Rime data from "%RIME_DATA_DIR%" - exit /b 1 -) - -set "PLUM_DATA_PREPARED=0" - -call :run_plum_install -if errorlevel 1 exit /b 1 - -if "%PLUM_DATA_PREPARED%"=="0" ( - echo [WARN] plum did not add preset shared data; falling back to Weasel shared data merge. - call :merge_weasel_shared_data - if errorlevel 1 ( - echo [ERROR] Failed to merge fallback Weasel shared data. - exit /b 1 - ) -) - -call :copy_opencc_data -if errorlevel 1 exit /b 1 - -if exist "%RIME_DATA_DIR%\PIME.yaml" ( - copy /Y "%RIME_DATA_DIR%\PIME.yaml" "%PACKAGE_RIME_DATA_DIR%\PIME.yaml" >nul -) - -echo [INFO] Packaged Rime shared data prepared at "%PACKAGE_RIME_DATA_DIR%" -exit /b 0 - -:merge_weasel_shared_data -call :find_weasel_data_dir -if not defined WEASEL_DATA_DIR ( - echo [WARN] Weasel shared data directory was not found; skip shared data merge. - exit /b 0 -) - -echo [INFO] Merging Weasel shared data from "%WEASEL_DATA_DIR%" -xcopy "%WEASEL_DATA_DIR%\*.*" "%PACKAGE_RIME_DATA_DIR%\" /E /I /Y /D >nul -if errorlevel 1 ( - echo [ERROR] Failed to merge Weasel shared data from "%WEASEL_DATA_DIR%" - exit /b 1 -) -exit /b 0 - -:find_weasel_data_dir -if defined WEASEL_DATA_DIR ( - if exist "%WEASEL_DATA_DIR%\default.yaml" exit /b 0 - set "WEASEL_DATA_DIR=" -) - -if defined WEASEL_ROOT ( - if exist "%WEASEL_ROOT%\data\default.yaml" ( - set "WEASEL_DATA_DIR=%WEASEL_ROOT%\data" - exit /b 0 - ) -) - -for %%K in ("HKLM\SOFTWARE\WOW6432Node\Rime\Weasel" "HKLM\SOFTWARE\Rime\Weasel" "HKCU\SOFTWARE\Rime\Weasel") do ( - for /f "tokens=2,*" %%A in ('reg query %%~K /v WeaselRoot 2^>nul ^| find "WeaselRoot"') do ( - set "WEASEL_ROOT=%%B" - if exist "%%B\data\default.yaml" ( - set "WEASEL_DATA_DIR=%%B\data" - exit /b 0 - ) - ) -) - -for %%D in ("%ProgramFiles%\Rime\weasel-*") do ( - if exist "%%~fD\data\default.yaml" set "WEASEL_DATA_DIR=%%~fD\data" -) -if defined WEASEL_DATA_DIR exit /b 0 - -if defined ProgramFiles(x86) ( - for %%D in ("%ProgramFiles(x86)%\Rime\weasel-*") do ( - if exist "%%~fD\data\default.yaml" set "WEASEL_DATA_DIR=%%~fD\data" - ) -) -exit /b 0 - -:run_plum_install -where bash >nul 2>nul -if errorlevel 1 ( - echo [WARN] bash was not found in PATH; skip plum/rime-install. - exit /b 0 -) - -if exist "%PLUM_INSTALL%" ( - echo [INFO] Running plum/rime-install %PLUM_PRESET_TARGET% ... - set "plum_dir=%PLUM_DIR%" - set "rime_dir=%PACKAGE_RIME_DATA_DIR%" - set "WSLENV=plum_dir:rime_dir" - pushd "%PLUM_DIR%" - bash rime-install %PLUM_PRESET_TARGET% - set "PLUM_EXIT=%ERRORLEVEL%" - popd - if not "%PLUM_EXIT%"=="0" ( - echo [WARN] plum/rime-install failed; will fall back to Weasel shared data merge. - exit /b 0 - ) - if exist "%PACKAGE_RIME_DATA_DIR%\essay.txt" ( - set "PLUM_DATA_PREPARED=1" - exit /b 0 - ) - echo [WARN] plum/rime-install completed but did not install preset shared data into "%PACKAGE_RIME_DATA_DIR%" -) - -if exist "%PLUM_INSTALL_BAT%" ( - echo [INFO] Running plum/rime-install.bat %PLUM_PRESET_TARGET% ... - set "rime_dir=%PACKAGE_RIME_DATA_DIR%" - pushd "%PLUM_DIR%" - call "%PLUM_INSTALL_BAT%" %PLUM_PRESET_TARGET% - set "PLUM_EXIT=%ERRORLEVEL%" - popd - if not "%PLUM_EXIT%"=="0" ( - echo [WARN] plum/rime-install.bat failed; will fall back to Weasel shared data merge. - exit /b 0 - ) - if exist "%PACKAGE_RIME_DATA_DIR%\essay.txt" ( - set "PLUM_DATA_PREPARED=1" - ) else ( - echo [WARN] plum/rime-install.bat completed but did not install preset shared data into "%PACKAGE_RIME_DATA_DIR%" - ) -) -exit /b 0 - -:copy_opencc_data -set "OPENCC_SOURCE=" -call :find_weasel_data_dir -if defined WEASEL_DATA_DIR if exist "%WEASEL_DATA_DIR%\opencc\*.ocd*" set "OPENCC_SOURCE=%WEASEL_DATA_DIR%\opencc" -if not defined OPENCC_SOURCE if exist "%RIME_DATA_DIR%\opencc\*.ocd*" set "OPENCC_SOURCE=%RIME_DATA_DIR%\opencc" - -if not defined OPENCC_SOURCE ( - echo [WARN] No OpenCC compiled data was found; packaged opencc directory remains unchanged. - exit /b 0 -) - -if not exist "%PACKAGE_RIME_DATA_DIR%\opencc" mkdir "%PACKAGE_RIME_DATA_DIR%\opencc" -xcopy "%OPENCC_SOURCE%\*.*" "%PACKAGE_RIME_DATA_DIR%\opencc\" /E /I /Y >nul -if errorlevel 1 ( - echo [ERROR] Failed to copy OpenCC data from "%OPENCC_SOURCE%" - exit /b 1 -) -echo [INFO] Copied OpenCC data from "%OPENCC_SOURCE%" -exit /b 0 diff --git a/go-backend/deploy-server.ps1 b/go-backend/deploy-server.ps1 deleted file mode 100644 index 159d3962b..000000000 --- a/go-backend/deploy-server.ps1 +++ /dev/null @@ -1,177 +0,0 @@ -param( - [string]$SourceRoot = (Join-Path $PSScriptRoot "build\go-backend"), - [string]$InstallRoot = "C:\Program Files (x86)\PIME\go-backend", - [string]$LauncherPath = "C:\Program Files (x86)\PIME\PIMELauncher.exe" -) - -$ErrorActionPreference = "Stop" - -function Test-Admin { - $identity = [Security.Principal.WindowsIdentity]::GetCurrent() - $principal = New-Object Security.Principal.WindowsPrincipal($identity) - return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) -} - -function Restart-Elevated { - $argumentList = @( - "-NoProfile" - "-ExecutionPolicy" - "Bypass" - "-File" - ('"{0}"' -f $PSCommandPath) - "-SourceRoot" - ('"{0}"' -f $SourceRoot) - "-InstallRoot" - ('"{0}"' -f $InstallRoot) - "-LauncherPath" - ('"{0}"' -f $LauncherPath) - ) - - Write-Host "[INFO] Requesting administrator privileges ..." - try { - Start-Process -FilePath "powershell.exe" -Verb RunAs -ArgumentList $argumentList | Out-Null - Write-Host "[INFO] Elevated PowerShell launched. Continuing in the administrator window." - exit 0 - } - catch { - throw "Administrator elevation was cancelled or failed." - } -} - -function Stop-PIMELauncher { - param([string]$Path) - - $running = @(Get-Process -Name "PIMELauncher" -ErrorAction SilentlyContinue) - if (-not $running) { - Write-Host "[INFO] PIMELauncher.exe is not running." - return - } - - Write-Host "[INFO] Stopping PIMELauncher.exe ..." - if (Test-Path -LiteralPath $Path) { - try { - Start-Process -FilePath $Path -ArgumentList "/quit" -WindowStyle Hidden | Out-Null - } - catch { - Write-Host "[WARN] Graceful quit failed, forcing stop." - } - } - - $deadline = (Get-Date).AddSeconds(5) - do { - Start-Sleep -Milliseconds 250 - $running = @(Get-Process -Name "PIMELauncher" -ErrorAction SilentlyContinue) - } while ($running.Count -gt 0 -and (Get-Date) -lt $deadline) - - if ($running.Count -gt 0) { - Write-Host "[WARN] Timed out waiting for graceful shutdown, forcing stop." - $running | Stop-Process -Force - } - - Start-Sleep -Seconds 1 - Write-Host "[INFO] PIMELauncher.exe stopped." -} - -function Start-PIMELauncher { - param([string]$Path) - - if (-not (Test-Path -LiteralPath $Path)) { - throw "PIMELauncher.exe not found: $Path" - } - - Write-Host "[INFO] Starting PIMELauncher.exe ..." - Start-Process -FilePath $Path | Out-Null - Write-Host "[INFO] PIMELauncher.exe started." -} - -function Write-FileDetails { - param( - [string]$Label, - [string]$Path - ) - - if (-not (Test-Path -LiteralPath $Path)) { - Write-Host ("[INFO] {0}: not found ({1})" -f $Label, $Path) - return - } - - $item = Get-Item -LiteralPath $Path - $version = $item.VersionInfo.FileVersion - if (-not $version) { - $version = "" - } - - Write-Host ("[INFO] {0}: LastWriteTime={1:yyyy-MM-dd HH:mm:ss}, FileVersion={2}" -f $Label, $item.LastWriteTime, $version) -} - -function Sync-GoBackendRuntime { - param( - [string]$Source, - [string]$Destination - ) - - Write-Host "[INFO] Syncing go-backend runtime directory ..." - - if (-not (Test-Path -LiteralPath $Destination)) { - New-Item -ItemType Directory -Path $Destination -Force | Out-Null - } - - $sourceServer = Join-Path $Source "server.exe" - $destinationServer = Join-Path $Destination "server.exe" - Copy-Item -LiteralPath $sourceServer -Destination $destinationServer -Force - - $sourceInputMethods = Join-Path $Source "input_methods" - $destinationInputMethods = Join-Path $Destination "input_methods" - if (Test-Path -LiteralPath $destinationInputMethods) { - Remove-Item -LiteralPath $destinationInputMethods -Recurse -Force - } - Copy-Item -LiteralPath $sourceInputMethods -Destination $destinationInputMethods -Recurse -Force - - Write-Host "[INFO] go-backend runtime directory synced." -} - -if (-not (Test-Admin)) { - Restart-Elevated -} - -if (-not (Test-Path -LiteralPath $SourceRoot)) { - throw "Source go-backend directory not found: $SourceRoot" -} - -if (-not (Test-Path -LiteralPath $InstallRoot)) { - throw "Install directory not found: $InstallRoot" -} - -$sourceServer = Join-Path $SourceRoot "server.exe" -$destinationServer = Join-Path $InstallRoot "server.exe" -$sourceRimeDLL = Join-Path $SourceRoot "input_methods\rime\rime.dll" -$destinationRimeDLL = Join-Path $InstallRoot "input_methods\rime\rime.dll" - -$sourceDataDir = Join-Path $SourceRoot "input_methods\rime\data" -$sourceIconsDir = Join-Path $SourceRoot "input_methods\rime\icons" -$sourceIMEJSON = Join-Path $SourceRoot "input_methods\rime\ime.json" - -$sourcePaths = @($sourceServer, $sourceRimeDLL, $sourceDataDir, $sourceIconsDir, $sourceIMEJSON) -foreach ($path in $sourcePaths) { - if (-not (Test-Path -LiteralPath $path)) { - throw "Required source path not found: $path" - } -} - -Write-FileDetails -Label "Source server.exe" -Path $sourceServer -Write-FileDetails -Label "Destination server.exe (before)" -Path $destinationServer -Write-FileDetails -Label "Source rime.dll" -Path $sourceRimeDLL -Write-FileDetails -Label "Destination rime.dll (before)" -Path $destinationRimeDLL - -try { - Stop-PIMELauncher -Path $LauncherPath - - Sync-GoBackendRuntime -Source $SourceRoot -Destination $InstallRoot - - Write-FileDetails -Label "Destination server.exe (after)" -Path $destinationServer - Write-FileDetails -Label "Destination rime.dll (after)" -Path $destinationRimeDLL -} -finally { - Start-PIMELauncher -Path $LauncherPath -} - \ No newline at end of file diff --git a/go-backend/go.mod b/go-backend/go.mod deleted file mode 100644 index 0bd407a34..000000000 --- a/go-backend/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/EasyIME/pime-go - -go 1.21 diff --git a/go-backend/input_methods/meow/ime.json b/go-backend/input_methods/meow/ime.json deleted file mode 100644 index 86ba86756..000000000 --- a/go-backend/input_methods/meow/ime.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "喵喵輸入法 (Go版)", - "version": "0.1", - "guid": "{7A1C2E93-5B64-4F88-AE21-3D9C6B70F145}", - "locale": "zh-Hans-CN", - "fallbackLocale": "zh-CN", - "icon": "", - "win8_icon": "", - "moduleName": "", - "serviceName": "" -} \ No newline at end of file diff --git a/go-backend/input_methods/meow/meow.go b/go-backend/input_methods/meow/meow.go deleted file mode 100644 index 79a209114..000000000 --- a/go-backend/input_methods/meow/meow.go +++ /dev/null @@ -1,329 +0,0 @@ -// 喵喵输入法 Go 实现 -// 每个输入法是一个独立的包 -package meow - -import ( - "log" - "unicode/utf8" - - "github.com/EasyIME/pime-go/pime" -) - -// IME 喵喵输入法结构 -type IME struct { - *pime.TextServiceBase - compositionString string - showCandidates bool - candidateCursor int - asciiMode bool -} - -// 候选词列表 -var candidates = []string{"喵", "描", "秒", "妙"} - -const ( - modeIconCommandID = 3000 - langIconCommandID = 3001 -) - -func normalizeLetterCharCode(keyCode, charCode int) int { - if charCode != 0 { - return charCode - } - if keyCode >= 0x41 && keyCode <= 0x5A { - return keyCode + 32 - } - return charCode -} - -func isModifierKey(keyCode int) bool { - switch keyCode { - case 0x10, // VK_SHIFT - 0x11, // VK_CONTROL - 0x12: // VK_MENU / Alt - return true - default: - return false - } -} - -func isPrintableKey(keyCode, charCode int) bool { - if charCode >= 0x20 { - return true - } - return keyCode >= 0x30 && keyCode <= 0x5A -} - -func trimLastRune(s string) string { - if s == "" { - return s - } - _, size := utf8.DecodeLastRuneInString(s) - return s[:len(s)-size] -} - -// New 创建喵喵输入法实例 -func New(client *pime.Client) pime.TextService { - return &IME{ - TextServiceBase: pime.NewTextServiceBase(client), - compositionString: "", - showCandidates: false, - candidateCursor: 0, - } -} - -// HandleRequest 处理请求 -func (ime *IME) HandleRequest(req *pime.Request) *pime.Response { - resp := pime.NewResponse(req.SeqNum, true) - - switch req.Method { - case "onActivate": - log.Println("喵喵输入法已激活") - pime.AddLangButtons(resp, ime.Client, ime.asciiMode, modeIconCommandID, langIconCommandID) - resp.ReturnValue = 1 - - case "onDeactivate": - log.Println("喵喵输入法已失活") - pime.RemoveLangButtons(resp, ime.Client) - resp.ReturnValue = 1 - - case "filterKeyDown": - return ime.filterKeyDown(req, resp) - - case "onKeyDown": - return ime.onKeyDown(req, resp) - - case "filterKeyUp": - resp.ReturnValue = 0 - - case "onCompositionTerminated": - // 清理状态 - ime.compositionString = "" - ime.showCandidates = false - ime.candidateCursor = 0 - - case "onCommand": - return ime.onCommand(req, resp) - } - - return resp -} - -// filterKeyDown 过滤按键 -func (ime *IME) filterKeyDown(req *pime.Request, resp *pime.Response) *pime.Response { - keyCode := req.KeyCode - charCode := normalizeLetterCharCode(keyCode, req.CharCode) - - if isModifierKey(keyCode) { - resp.ReturnValue = 0 - return resp - } - - if ime.asciiMode && ime.compositionString == "" && !ime.showCandidates && isPrintableKey(keyCode, charCode) { - resp.ReturnValue = 0 - return resp - } - - if ime.showCandidates { - switch keyCode { - case 0x26, // VK_UP - 0x1B: // VK_ESCAPE - resp.ReturnValue = 1 - return resp - } - if keyCode >= 0x31 && keyCode <= 0x34 { - resp.ReturnValue = 1 - return resp - } - } - - // 如果组合字符串为空,不处理某些按键 - if ime.compositionString == "" { - switch keyCode { - case 0x0D, // VK_RETURN - 0x08, // VK_BACK - 0x25, // VK_LEFT - 0x26, // VK_UP - 0x27, // VK_DOWN - 0x28: // VK_RIGHT - resp.ReturnValue = 0 // 不处理 - return resp - } - } - - if ime.compositionString != "" { - switch keyCode { - case 0x0D, // VK_RETURN - 0x08, // VK_BACK - 0x1B, // VK_ESCAPE - 0x25, // VK_LEFT - 0x27, // VK_RIGHT - 0x28: // VK_DOWN - resp.ReturnValue = 1 - return resp - } - } - - if isPrintableKey(keyCode, charCode) { - resp.ReturnValue = 1 - return resp - } - - resp.ReturnValue = 1 // 处理 - return resp -} - -// onKeyDown 处理按键 -func (ime *IME) onKeyDown(req *pime.Request, resp *pime.Response) *pime.Response { - keyCode := req.KeyCode - charCode := normalizeLetterCharCode(keyCode, req.CharCode) - - if ime.asciiMode && ime.compositionString == "" && !ime.showCandidates && isPrintableKey(keyCode, charCode) { - resp.ReturnValue = 0 - return resp - } - - if isModifierKey(keyCode) { - resp.ReturnValue = 0 - return resp - } - - if ime.showCandidates { - switch keyCode { - case 0x26, // VK_UP - 0x1B: // VK_ESCAPE - ime.showCandidates = false - resp.ShowCandidates = false - resp.CompositionString = ime.compositionString - resp.ReturnValue = 1 - return resp - } - } - - // 处理 'm' 键 (109 或 77) - if charCode == 109 || charCode == 77 { - // 添加 '喵' 到组合字符串 - if ime.compositionString == "" { - // 第一次按 'm' - ime.compositionString = "喵" - resp.CompositionString = ime.compositionString - resp.ReturnValue = 1 - } else { - // 重复按 'm',显示候选词 - ime.showCandidates = true - resp.CompositionString = ime.compositionString - resp.CandidateList = candidates - resp.ShowCandidates = true - resp.ReturnValue = 1 - } - return resp - } - - if keyCode == 0x28 && ime.compositionString != "" { // VK_DOWN - ime.showCandidates = true - resp.CompositionString = ime.compositionString - resp.CandidateList = candidates - resp.ShowCandidates = true - resp.ReturnValue = 1 - return resp - } - - // 处理数字键选择候选词 - if keyCode >= 0x31 && keyCode <= 0x34 { // '1' - '4' - if ime.showCandidates { - index := int(keyCode - 0x31) - if index < len(candidates) { - // 提交选中的候选词 - resp.CommitString = candidates[index] - // 重置状态 - ime.compositionString = "" - ime.showCandidates = false - resp.CompositionString = "" - resp.ShowCandidates = false - resp.ReturnValue = 1 - return resp - } - } - } - - // 处理回车键 - 提交 - if keyCode == 0x0D { // VK_RETURN - if ime.compositionString != "" { - resp.CommitString = ime.compositionString - // 重置状态 - ime.compositionString = "" - ime.showCandidates = false - resp.CompositionString = "" - resp.ShowCandidates = false - resp.ReturnValue = 1 - return resp - } - } - - // 处理退格键 - if keyCode == 0x08 { // VK_BACK - if ime.compositionString != "" { - // 删除最后一个字符,让测试输入更接近 Python 版本。 - ime.compositionString = trimLastRune(ime.compositionString) - ime.showCandidates = false - resp.CompositionString = ime.compositionString - resp.ShowCandidates = false - resp.ReturnValue = 1 - return resp - } - } - - // 处理 Escape 键 - if keyCode == 0x1B { // VK_ESCAPE - if ime.compositionString != "" { - // 取消输入 - ime.compositionString = "" - ime.showCandidates = false - resp.CompositionString = "" - resp.ShowCandidates = false - resp.ReturnValue = 1 - return resp - } - } - - if isPrintableKey(keyCode, charCode) { - ime.compositionString += "喵" - ime.showCandidates = false - resp.CompositionString = ime.compositionString - resp.ShowCandidates = false - resp.ReturnValue = 1 - return resp - } - - // 其他按键不处理 - resp.ReturnValue = 0 - return resp -} - -func (ime *IME) onCommand(req *pime.Request, resp *pime.Response) *pime.Response { - commandID, ok := req.Data["commandId"].(float64) - if !ok { - resp.ReturnValue = 0 - return resp - } - - switch int(commandID) { - case modeIconCommandID, langIconCommandID: - ime.asciiMode = !ime.asciiMode - pime.ChangeLangButtons(resp, ime.Client, ime.asciiMode) - resp.ReturnValue = 1 - default: - resp.ReturnValue = 0 - } - return resp -} - -// Init 初始化 -func (ime *IME) Init(req *pime.Request) bool { - return true -} - -// Close 关闭 -func (ime *IME) Close() { - // 清理资源 -} diff --git a/go-backend/input_methods/meow/meow_test.go b/go-backend/input_methods/meow/meow_test.go deleted file mode 100644 index ec629bb53..000000000 --- a/go-backend/input_methods/meow/meow_test.go +++ /dev/null @@ -1,288 +0,0 @@ -package meow - -import ( - "testing" - - "github.com/EasyIME/pime-go/pime" -) - -func newTestIME() *IME { - return New(&pime.Client{ID: "test-client"}).(*IME) -} - -func TestNewInitialState(t *testing.T) { - ime := newTestIME() - - if ime.compositionString != "" { - t.Fatalf("expected empty composition, got %q", ime.compositionString) - } - if ime.showCandidates { - t.Fatal("expected candidates to be hidden initially") - } - if ime.candidateCursor != 0 { - t.Fatalf("expected candidate cursor 0, got %d", ime.candidateCursor) - } -} - -func TestFilterKeyDownWhenCompositionEmpty(t *testing.T) { - ime := newTestIME() - - tests := []struct { - name string - keyCode int - charCode int - want int - }{ - {name: "enter is ignored", keyCode: 0x0D, want: 0}, - {name: "backspace is ignored", keyCode: 0x08, want: 0}, - {name: "left is ignored", keyCode: 0x25, want: 0}, - {name: "up is ignored", keyCode: 0x26, want: 0}, - {name: "down is ignored when empty", keyCode: 0x28, want: 0}, - {name: "right is ignored", keyCode: 0x27, want: 0}, - {name: "shift is ignored", keyCode: 0x10, want: 0}, - {name: "letter is handled", keyCode: 0x4D, charCode: 'm', want: 1}, - {name: "other printable letter is handled", keyCode: 0x46, charCode: 'f', want: 1}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - resp := ime.filterKeyDown(&pime.Request{KeyCode: tt.keyCode, CharCode: tt.charCode}, pime.NewResponse(1, true)) - if resp.ReturnValue != tt.want { - t.Fatalf("expected returnValue %d, got %d", tt.want, resp.ReturnValue) - } - }) - } -} - -func TestFilterKeyDownWhenCompositionExists(t *testing.T) { - ime := newTestIME() - ime.compositionString = "喵" - - resp := ime.filterKeyDown(&pime.Request{KeyCode: 0x0D}, pime.NewResponse(1, true)) - if resp.ReturnValue != 1 { - t.Fatalf("expected enter to be handled while composing, got %d", resp.ReturnValue) - } -} - -func TestOnKeyDownFirstMStartsComposition(t *testing.T) { - ime := newTestIME() - - resp := ime.onKeyDown(&pime.Request{SeqNum: 1, CharCode: 'm', KeyCode: 0x4D}, pime.NewResponse(1, true)) - - if resp.ReturnValue != 1 { - t.Fatalf("expected first m to be handled, got %d", resp.ReturnValue) - } - if resp.CompositionString != "喵" { - t.Fatalf("expected composition 喵, got %q", resp.CompositionString) - } - if resp.ShowCandidates { - t.Fatal("expected candidates to stay hidden on first m") - } - if ime.compositionString != "喵" { - t.Fatalf("expected ime state to store 喵, got %q", ime.compositionString) - } -} - -func TestOnKeyDownUppercaseMAlsoStartsComposition(t *testing.T) { - ime := newTestIME() - - resp := ime.onKeyDown(&pime.Request{SeqNum: 1, CharCode: 'M', KeyCode: 0x4D}, pime.NewResponse(1, true)) - - if resp.ReturnValue != 1 { - t.Fatalf("expected uppercase M to be handled, got %d", resp.ReturnValue) - } - if resp.CompositionString != "喵" { - t.Fatalf("expected composition 喵, got %q", resp.CompositionString) - } -} - -func TestOnKeyDownFallsBackToKeyCodeWhenCharCodeMissing(t *testing.T) { - ime := newTestIME() - - resp := ime.onKeyDown(&pime.Request{SeqNum: 1, KeyCode: 0x4D}, pime.NewResponse(1, true)) - - if resp.ReturnValue != 1 { - t.Fatalf("expected keyCode-only M to be handled, got %d", resp.ReturnValue) - } - if resp.CompositionString != "喵" { - t.Fatalf("expected composition 喵 from keyCode fallback, got %q", resp.CompositionString) - } -} - -func TestOnKeyDownSecondMShowsCandidates(t *testing.T) { - ime := newTestIME() - ime.compositionString = "喵" - - resp := ime.onKeyDown(&pime.Request{SeqNum: 2, CharCode: 'm', KeyCode: 0x4D}, pime.NewResponse(2, true)) - - if resp.ReturnValue != 1 { - t.Fatalf("expected second m to be handled, got %d", resp.ReturnValue) - } - if resp.CompositionString != "喵" { - t.Fatalf("expected composition to remain 喵, got %q", resp.CompositionString) - } - if !resp.ShowCandidates { - t.Fatal("expected candidates to be shown on repeated m") - } - if len(resp.CandidateList) != len(candidates) { - t.Fatalf("expected %d candidates, got %d", len(candidates), len(resp.CandidateList)) - } - for i, candidate := range candidates { - if resp.CandidateList[i] != candidate { - t.Fatalf("candidate %d mismatch: want %q, got %q", i, candidate, resp.CandidateList[i]) - } - } - if !ime.showCandidates { - t.Fatal("expected ime state to track candidate visibility") - } -} - -func TestOnKeyDownNumberSelectsCandidate(t *testing.T) { - ime := newTestIME() - ime.compositionString = "喵" - ime.showCandidates = true - - resp := ime.onKeyDown(&pime.Request{SeqNum: 3, KeyCode: 0x32}, pime.NewResponse(3, true)) - - if resp.ReturnValue != 1 { - t.Fatalf("expected number key to be handled, got %d", resp.ReturnValue) - } - if resp.CommitString != "描" { - t.Fatalf("expected second candidate 描, got %q", resp.CommitString) - } - if resp.CompositionString != "" { - t.Fatalf("expected composition to be cleared, got %q", resp.CompositionString) - } - if resp.ShowCandidates { - t.Fatal("expected candidates to be hidden after commit") - } - if ime.compositionString != "" || ime.showCandidates { - t.Fatal("expected ime state to reset after candidate selection") - } -} - -func TestOnKeyDownEnterCommitsComposition(t *testing.T) { - ime := newTestIME() - ime.compositionString = "喵" - ime.showCandidates = true - - resp := ime.onKeyDown(&pime.Request{SeqNum: 4, KeyCode: 0x0D}, pime.NewResponse(4, true)) - - if resp.ReturnValue != 1 { - t.Fatalf("expected enter to be handled, got %d", resp.ReturnValue) - } - if resp.CommitString != "喵" { - t.Fatalf("expected enter to commit 喵, got %q", resp.CommitString) - } - if resp.CompositionString != "" { - t.Fatalf("expected composition to clear, got %q", resp.CompositionString) - } - if resp.ShowCandidates { - t.Fatal("expected candidates to be hidden after enter") - } - if ime.compositionString != "" || ime.showCandidates { - t.Fatal("expected ime state to reset after enter") - } -} - -func TestOnKeyDownBackspaceClearsComposition(t *testing.T) { - ime := newTestIME() - ime.compositionString = "喵喵" - ime.showCandidates = true - - resp := ime.onKeyDown(&pime.Request{SeqNum: 5, KeyCode: 0x08}, pime.NewResponse(5, true)) - - if resp.ReturnValue != 1 { - t.Fatalf("expected backspace to be handled, got %d", resp.ReturnValue) - } - if resp.CompositionString != "喵" { - t.Fatalf("expected composition to shrink, got %q", resp.CompositionString) - } - if resp.ShowCandidates { - t.Fatal("expected candidates to be hidden after backspace") - } - if ime.compositionString != "喵" || ime.showCandidates { - t.Fatal("expected ime state to keep remaining composition after backspace") - } -} - -func TestOnKeyDownEscapeCancelsComposition(t *testing.T) { - ime := newTestIME() - ime.compositionString = "喵" - ime.showCandidates = true - - resp := ime.onKeyDown(&pime.Request{SeqNum: 6, KeyCode: 0x1B}, pime.NewResponse(6, true)) - - if resp.ReturnValue != 1 { - t.Fatalf("expected escape to be handled, got %d", resp.ReturnValue) - } - if resp.CompositionString != "喵" { - t.Fatalf("expected composition to remain while closing candidate list, got %q", resp.CompositionString) - } - if resp.ShowCandidates { - t.Fatal("expected candidates to be hidden after escape") - } - if ime.compositionString != "喵" || ime.showCandidates { - t.Fatal("expected ime state to keep composition but hide candidates after escape") - } -} - -func TestOnKeyDownUnhandledKeyReturnsZero(t *testing.T) { - ime := newTestIME() - - resp := ime.onKeyDown(&pime.Request{SeqNum: 7, KeyCode: 0x10}, pime.NewResponse(7, true)) - if resp.ReturnValue != 0 { - t.Fatalf("expected modifier key to be ignored, got %d", resp.ReturnValue) - } -} - -func TestOnKeyDownOtherPrintableKeyAlsoBuildsComposition(t *testing.T) { - ime := newTestIME() - - resp := ime.onKeyDown(&pime.Request{SeqNum: 8, KeyCode: 0x46, CharCode: 'f'}, pime.NewResponse(8, true)) - if resp.ReturnValue != 1 { - t.Fatalf("expected printable key to be handled, got %d", resp.ReturnValue) - } - if resp.CompositionString != "喵" { - t.Fatalf("expected printable key to append 喵, got %q", resp.CompositionString) - } -} - -func TestOnKeyDownBackspaceRemovesLastRune(t *testing.T) { - ime := newTestIME() - ime.compositionString = "喵喵" - ime.showCandidates = true - - resp := ime.onKeyDown(&pime.Request{SeqNum: 9, KeyCode: 0x08}, pime.NewResponse(9, true)) - if resp.ReturnValue != 1 { - t.Fatalf("expected backspace to be handled, got %d", resp.ReturnValue) - } - if resp.CompositionString != "喵" { - t.Fatalf("expected composition to shrink to one rune, got %q", resp.CompositionString) - } - if ime.compositionString != "喵" { - t.Fatalf("expected ime composition to shrink, got %q", ime.compositionString) - } -} - -func TestHandleRequestCompositionTerminatedResetsState(t *testing.T) { - ime := newTestIME() - ime.compositionString = "喵" - ime.showCandidates = true - ime.candidateCursor = 2 - - resp := ime.HandleRequest(&pime.Request{SeqNum: 8, Method: "onCompositionTerminated"}) - - if !resp.Success { - t.Fatal("expected composition termination response to succeed") - } - if ime.compositionString != "" { - t.Fatalf("expected composition to reset, got %q", ime.compositionString) - } - if ime.showCandidates { - t.Fatal("expected candidates to reset") - } - if ime.candidateCursor != 0 { - t.Fatalf("expected cursor to reset, got %d", ime.candidateCursor) - } -} diff --git a/go-backend/input_methods/rime/icon.ico b/go-backend/input_methods/rime/icon.ico deleted file mode 100644 index e715ae17a..000000000 Binary files a/go-backend/input_methods/rime/icon.ico and /dev/null differ diff --git a/go-backend/input_methods/rime/icons/chi.ico b/go-backend/input_methods/rime/icons/chi.ico deleted file mode 100644 index 00ce23397..000000000 Binary files a/go-backend/input_methods/rime/icons/chi.ico and /dev/null differ diff --git a/go-backend/input_methods/rime/icons/chi_full_capsoff.ico b/go-backend/input_methods/rime/icons/chi_full_capsoff.ico deleted file mode 100644 index 7c28f6bde..000000000 Binary files a/go-backend/input_methods/rime/icons/chi_full_capsoff.ico and /dev/null differ diff --git a/go-backend/input_methods/rime/icons/chi_full_capson.ico b/go-backend/input_methods/rime/icons/chi_full_capson.ico deleted file mode 100644 index 68f45d39d..000000000 Binary files a/go-backend/input_methods/rime/icons/chi_full_capson.ico and /dev/null differ diff --git a/go-backend/input_methods/rime/icons/chi_half_capsoff.ico b/go-backend/input_methods/rime/icons/chi_half_capsoff.ico deleted file mode 100644 index 17241e9fd..000000000 Binary files a/go-backend/input_methods/rime/icons/chi_half_capsoff.ico and /dev/null differ diff --git a/go-backend/input_methods/rime/icons/chi_half_capson.ico b/go-backend/input_methods/rime/icons/chi_half_capson.ico deleted file mode 100644 index fc85da5c6..000000000 Binary files a/go-backend/input_methods/rime/icons/chi_half_capson.ico and /dev/null differ diff --git a/go-backend/input_methods/rime/icons/config.ico b/go-backend/input_methods/rime/icons/config.ico deleted file mode 100644 index c5b265246..000000000 Binary files a/go-backend/input_methods/rime/icons/config.ico and /dev/null differ diff --git a/go-backend/input_methods/rime/icons/eng.ico b/go-backend/input_methods/rime/icons/eng.ico deleted file mode 100644 index cf68a5369..000000000 Binary files a/go-backend/input_methods/rime/icons/eng.ico and /dev/null differ diff --git a/go-backend/input_methods/rime/icons/eng_full_capsoff.ico b/go-backend/input_methods/rime/icons/eng_full_capsoff.ico deleted file mode 100644 index b6c921555..000000000 Binary files a/go-backend/input_methods/rime/icons/eng_full_capsoff.ico and /dev/null differ diff --git a/go-backend/input_methods/rime/icons/eng_full_capson.ico b/go-backend/input_methods/rime/icons/eng_full_capson.ico deleted file mode 100644 index 70fc2277d..000000000 Binary files a/go-backend/input_methods/rime/icons/eng_full_capson.ico and /dev/null differ diff --git a/go-backend/input_methods/rime/icons/eng_half_capsoff.ico b/go-backend/input_methods/rime/icons/eng_half_capsoff.ico deleted file mode 100644 index 9f1a6c61e..000000000 Binary files a/go-backend/input_methods/rime/icons/eng_half_capsoff.ico and /dev/null differ diff --git a/go-backend/input_methods/rime/icons/eng_half_capson.ico b/go-backend/input_methods/rime/icons/eng_half_capson.ico deleted file mode 100644 index 3b6fcb3c2..000000000 Binary files a/go-backend/input_methods/rime/icons/eng_half_capson.ico and /dev/null differ diff --git a/go-backend/input_methods/rime/icons/full.ico b/go-backend/input_methods/rime/icons/full.ico deleted file mode 100644 index 5fcc68e28..000000000 Binary files a/go-backend/input_methods/rime/icons/full.ico and /dev/null differ diff --git a/go-backend/input_methods/rime/icons/half.ico b/go-backend/input_methods/rime/icons/half.ico deleted file mode 100644 index 9686af707..000000000 Binary files a/go-backend/input_methods/rime/icons/half.ico and /dev/null differ diff --git a/go-backend/input_methods/rime/icons/zh.ico b/go-backend/input_methods/rime/icons/zh.ico deleted file mode 100644 index 1c931077c..000000000 Binary files a/go-backend/input_methods/rime/icons/zh.ico and /dev/null differ diff --git a/go-backend/input_methods/rime/ime.json b/go-backend/input_methods/rime/ime.json deleted file mode 100644 index 62366857d..000000000 --- a/go-backend/input_methods/rime/ime.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name":"中州韻輸入法 (PIME - Go)", - "version": "0.1", - "guid": "{3F6B5A12-8D44-4E71-9A2E-6B4F9C1D2A30}", - "locale": "zh-Hans-CN", - "fallbackLocale": "zh-CN", - "icon": "", - "win8_icon": "", - "moduleName": "rime", - "serviceName": "RimeTextService" -} diff --git a/go-backend/input_methods/rime/librime.go b/go-backend/input_methods/rime/librime.go deleted file mode 100644 index 58a60bc5b..000000000 --- a/go-backend/input_methods/rime/librime.go +++ /dev/null @@ -1,459 +0,0 @@ -//go:build windows - -// RIME Windows DLL 动态加载封装 -// 参考 python/librime.py -package rime - -import ( - "fmt" - "log" - "os" - "path/filepath" - "runtime" - "sync" - "syscall" - "unsafe" -) - -const ( - RIME_MAX_NUM_CANDIDATES = 10 -) - -type RimeSessionId uintptr - -type RimeTraits struct { - SharedDataDir string - UserDataDir string - DistributionName string - DistributionCodeName string - DistributionVersion string - AppName string - Modules []string -} - -type RimeComposition struct { - Length int - CursorPos int - SelStart int - SelEnd int - Preedit string -} - -type RimeCandidate struct { - Text string - Comment string -} - -type RimeMenu struct { - PageSize int - PageNo int - IsLastPage bool - HighlightedCandidateIndex int - NumCandidates int - Candidates []RimeCandidate - SelectKeys string -} - -type RimeCommit struct { - Text string -} - -type NotificationHandler func(session RimeSessionId, messageType, messageValue string) - -type rimeTraitsC struct { - DataSize int32 - SharedDataDir *byte - UserDataDir *byte - DistributionName *byte - DistributionCodeName *byte - DistributionVersion *byte - AppName *byte - Modules **byte -} - -type rimeCompositionC struct { - Length int32 - CursorPos int32 - SelStart int32 - SelEnd int32 - Preedit *byte -} - -type rimeCandidateC struct { - Text *byte - Comment *byte - Reserved uintptr -} - -type rimeMenuC struct { - PageSize int32 - PageNo int32 - IsLastPage int32 - HighlightedCandidateIndex int32 - NumCandidates int32 - Candidates *rimeCandidateC - SelectKeys *byte -} - -type rimeCommitC struct { - DataSize int32 - Text *byte -} - -type rimeContextC struct { - DataSize int32 - Composition rimeCompositionC - Menu rimeMenuC - CommitTextPreview *byte - SelectLabels **byte -} - -var ( - rimeDLLMu sync.Mutex - rimeDLL *syscall.LazyDLL - rimeProcs struct { - setup *syscall.LazyProc - initialize *syscall.LazyProc - finalize *syscall.LazyProc - startMaintenance *syscall.LazyProc - joinMaintenanceThread *syscall.LazyProc - deployConfigFile *syscall.LazyProc - createSession *syscall.LazyProc - findSession *syscall.LazyProc - destroySession *syscall.LazyProc - processKey *syscall.LazyProc - clearComposition *syscall.LazyProc - getCommit *syscall.LazyProc - freeCommit *syscall.LazyProc - getContext *syscall.LazyProc - freeContext *syscall.LazyProc - setOption *syscall.LazyProc - getOption *syscall.LazyProc - getVersion *syscall.LazyProc - } -) - -func loadRimeDLL(dllPath string) error { - rimeDLLMu.Lock() - defer rimeDLLMu.Unlock() - - if rimeDLL != nil { - return nil - } - - if dllPath == "" { - dllPath = "rime.dll" - } - dll := syscall.NewLazyDLL(dllPath) - procs := struct { - setup *syscall.LazyProc - initialize *syscall.LazyProc - finalize *syscall.LazyProc - startMaintenance *syscall.LazyProc - joinMaintenanceThread *syscall.LazyProc - deployConfigFile *syscall.LazyProc - createSession *syscall.LazyProc - findSession *syscall.LazyProc - destroySession *syscall.LazyProc - processKey *syscall.LazyProc - clearComposition *syscall.LazyProc - getCommit *syscall.LazyProc - freeCommit *syscall.LazyProc - getContext *syscall.LazyProc - freeContext *syscall.LazyProc - setOption *syscall.LazyProc - getOption *syscall.LazyProc - getVersion *syscall.LazyProc - }{ - setup: dll.NewProc("RimeSetup"), - initialize: dll.NewProc("RimeInitialize"), - finalize: dll.NewProc("RimeFinalize"), - startMaintenance: dll.NewProc("RimeStartMaintenance"), - joinMaintenanceThread: dll.NewProc("RimeJoinMaintenanceThread"), - deployConfigFile: dll.NewProc("RimeDeployConfigFile"), - createSession: dll.NewProc("RimeCreateSession"), - findSession: dll.NewProc("RimeFindSession"), - destroySession: dll.NewProc("RimeDestroySession"), - processKey: dll.NewProc("RimeProcessKey"), - clearComposition: dll.NewProc("RimeClearComposition"), - getCommit: dll.NewProc("RimeGetCommit"), - freeCommit: dll.NewProc("RimeFreeCommit"), - getContext: dll.NewProc("RimeGetContext"), - freeContext: dll.NewProc("RimeFreeContext"), - setOption: dll.NewProc("RimeSetOption"), - getOption: dll.NewProc("RimeGetOption"), - getVersion: dll.NewProc("RimeGetVersion"), - } - - for _, proc := range []*syscall.LazyProc{ - procs.setup, procs.initialize, procs.finalize, procs.startMaintenance, procs.joinMaintenanceThread, - procs.deployConfigFile, procs.createSession, procs.findSession, procs.destroySession, procs.processKey, - procs.clearComposition, procs.getCommit, procs.freeCommit, procs.getContext, procs.freeContext, - procs.setOption, procs.getOption, - } { - if err := proc.Find(); err != nil { - return err - } - } - - rimeDLL = dll - rimeProcs = procs - return nil -} - -func utf8Ptr(s string) *byte { - if s == "" { - return nil - } - ptr, _ := syscall.BytePtrFromString(s) - return ptr -} - -func cString(ptr *byte) string { - if ptr == nil { - return "" - } - bytes := make([]byte, 0, 32) - for p := uintptr(unsafe.Pointer(ptr)); ; p++ { - b := *(*byte)(unsafe.Pointer(p)) - if b == 0 { - break - } - bytes = append(bytes, b) - } - return string(bytes) -} - -func boolResult(r1 uintptr) bool { - return r1 != 0 -} - -func Init(traits RimeTraits) bool { - cTraits := rimeTraitsC{ - DataSize: int32(unsafe.Sizeof(rimeTraitsC{})) - 4, - SharedDataDir: utf8Ptr(traits.SharedDataDir), - UserDataDir: utf8Ptr(traits.UserDataDir), - DistributionName: utf8Ptr(traits.DistributionName), - DistributionCodeName: utf8Ptr(traits.DistributionCodeName), - DistributionVersion: utf8Ptr(traits.DistributionVersion), - AppName: utf8Ptr(traits.AppName), - } - - r1, _, _ := rimeProcs.setup.Call(uintptr(unsafe.Pointer(&cTraits))) - runtime.KeepAlive(cTraits) - return boolResult(r1) || true -} - -func Finalize() { - rimeProcs.finalize.Call() -} - -func StartSession() (RimeSessionId, bool) { - r1, _, _ := rimeProcs.createSession.Call() - return RimeSessionId(r1), r1 != 0 -} - -func FindSession(sessionId RimeSessionId) bool { - if sessionId == 0 { - return false - } - r1, _, _ := rimeProcs.findSession.Call(uintptr(sessionId)) - return boolResult(r1) -} - -func EndSession(sessionId RimeSessionId) { - if sessionId != 0 { - rimeProcs.destroySession.Call(uintptr(sessionId)) - } -} - -func ProcessKey(sessionId RimeSessionId, keyCode, modifiers int) bool { - r1, _, _ := rimeProcs.processKey.Call(uintptr(sessionId), uintptr(keyCode), uintptr(modifiers)) - return boolResult(r1) -} - -func ClearComposition(sessionId RimeSessionId) { - rimeProcs.clearComposition.Call(uintptr(sessionId)) -} - -func GetComposition(sessionId RimeSessionId) (RimeComposition, bool) { - context, ok := getContext(sessionId) - if !ok { - return RimeComposition{}, false - } - defer freeContext(&context) - - return RimeComposition{ - Length: int(context.Composition.Length), - CursorPos: int(context.Composition.CursorPos), - SelStart: int(context.Composition.SelStart), - SelEnd: int(context.Composition.SelEnd), - Preedit: cString(context.Composition.Preedit), - }, true -} - -func GetMenu(sessionId RimeSessionId) (RimeMenu, bool) { - context, ok := getContext(sessionId) - if !ok { - return RimeMenu{}, false - } - defer freeContext(&context) - - menu := RimeMenu{ - PageSize: int(context.Menu.PageSize), - PageNo: int(context.Menu.PageNo), - IsLastPage: context.Menu.IsLastPage != 0, - HighlightedCandidateIndex: int(context.Menu.HighlightedCandidateIndex), - NumCandidates: int(context.Menu.NumCandidates), - SelectKeys: cString(context.Menu.SelectKeys), - } - - if context.Menu.NumCandidates > 0 && context.Menu.Candidates != nil { - candidates := unsafe.Slice(context.Menu.Candidates, int(context.Menu.NumCandidates)) - menu.Candidates = make([]RimeCandidate, 0, len(candidates)) - for _, candidate := range candidates { - menu.Candidates = append(menu.Candidates, RimeCandidate{ - Text: cString(candidate.Text), - Comment: cString(candidate.Comment), - }) - } - } - return menu, true -} - -func GetCommit(sessionId RimeSessionId) (RimeCommit, bool) { - commit := rimeCommitC{DataSize: int32(unsafe.Sizeof(rimeCommitC{})) - 4} - r1, _, _ := rimeProcs.getCommit.Call(uintptr(sessionId), uintptr(unsafe.Pointer(&commit))) - if !boolResult(r1) { - return RimeCommit{}, false - } - defer rimeProcs.freeCommit.Call(uintptr(unsafe.Pointer(&commit))) - return RimeCommit{Text: cString(commit.Text)}, true -} - -func SetOption(sessionId RimeSessionId, option string, value bool) { - name := utf8Ptr(option) - var v uintptr - if value { - v = 1 - } - rimeProcs.setOption.Call(uintptr(sessionId), uintptr(unsafe.Pointer(name)), v) - runtime.KeepAlive(name) -} - -func GetOption(sessionId RimeSessionId, option string) bool { - name := utf8Ptr(option) - r1, _, _ := rimeProcs.getOption.Call(uintptr(sessionId), uintptr(unsafe.Pointer(name))) - runtime.KeepAlive(name) - return boolResult(r1) -} - -func SelectCandidate(sessionId RimeSessionId, index int) { - _ = sessionId - _ = index -} - -func SelectPage(sessionId RimeSessionId, pageNo int) { - _ = sessionId - _ = pageNo -} - -func DeployConfigFile(filePath, key string) bool { - cFile := utf8Ptr(filePath) - cKey := utf8Ptr(key) - r1, _, _ := rimeProcs.deployConfigFile.Call(uintptr(unsafe.Pointer(cFile)), uintptr(unsafe.Pointer(cKey))) - runtime.KeepAlive(cFile) - runtime.KeepAlive(cKey) - return boolResult(r1) -} - -func SetNotificationHandler(handler NotificationHandler) { - _ = handler -} - -func APIVersion() string { - return "" -} - -func GetName() string { - return "" -} - -func GetVersion() string { - if rimeProcs.getVersion == nil { - return "" - } - if err := rimeProcs.getVersion.Find(); err != nil { - return "" - } - r1, _, _ := rimeProcs.getVersion.Call() - return cString((*byte)(unsafe.Pointer(r1))) -} - -func RimeInit(datadir, userdir, appname, appver string, fullcheck bool) bool { - if err := os.MkdirAll(userdir, 0700); err != nil { - log.Printf("创建用户目录失败: %v", err) - return false - } - - dllPath := filepath.Join(filepath.Dir(datadir), "rime.dll") - if _, err := os.Stat(dllPath); err != nil { - dllPath = "rime.dll" - } - if err := loadRimeDLL(dllPath); err != nil { - log.Printf("加载 RIME DLL 失败: %v", err) - return false - } - - traits := RimeTraits{ - SharedDataDir: datadir, - UserDataDir: userdir, - DistributionName: "Rime", - DistributionCodeName: appname, - DistributionVersion: appver, - AppName: fmt.Sprintf("Rime.%s", appname), - } - if !Init(traits) { - log.Println("RIME setup 失败") - return false - } - - rimeProcs.initialize.Call(0) - var fullcheckArg uintptr - if fullcheck { - fullcheckArg = 1 - } - r1, _, _ := rimeProcs.startMaintenance.Call(fullcheckArg) - if boolResult(r1) { - rimeProcs.joinMaintenanceThread.Call() - } - - configFiles := []string{ - filepath.Join(datadir, appname+".yaml"), - filepath.Join(userdir, appname+".yaml"), - filepath.Join(userdir, "default.custom.yaml"), - } - for _, configFile := range configFiles { - if _, err := os.Stat(configFile); err != nil { - continue - } - if !DeployConfigFile(configFile, "config_version") { - log.Printf("部署配置文件失败: %s", configFile) - return false - } - } - return true -} - -func getContext(sessionId RimeSessionId) (rimeContextC, bool) { - context := rimeContextC{DataSize: int32(unsafe.Sizeof(rimeContextC{})) - 4} - r1, _, _ := rimeProcs.getContext.Call(uintptr(sessionId), uintptr(unsafe.Pointer(&context))) - if !boolResult(r1) { - return rimeContextC{}, false - } - return context, true -} - -func freeContext(context *rimeContextC) { - rimeProcs.freeContext.Call(uintptr(unsafe.Pointer(context))) -} diff --git a/go-backend/input_methods/rime/native_cgo.go b/go-backend/input_methods/rime/native_cgo.go deleted file mode 100644 index 0b94de52f..000000000 --- a/go-backend/input_methods/rime/native_cgo.go +++ /dev/null @@ -1,108 +0,0 @@ -//go:build windows - -package rime - -import ( - "log" - "sync" - - "github.com/EasyIME/pime-go/pime" -) - -type nativeBackend struct { - sessionID RimeSessionId -} - -var ( - rimeInitOnce sync.Once - rimeInitOK bool -) - -func newNativeBackend() rimeBackend { - return &nativeBackend{} -} - -func (b *nativeBackend) Initialize(sharedDir, userDir string, firstRun bool) bool { - rimeInitOnce.Do(func() { - rimeInitOK = RimeInit(sharedDir, userDir, APP, APP_VERSION, firstRun) - if !rimeInitOK { - log.Println("RIME 初始化失败,原生后端不可用") - } - }) - return rimeInitOK -} - -func (b *nativeBackend) EnsureSession() bool { - if b.sessionID != 0 && FindSession(b.sessionID) { - return true - } - sessionID, ok := StartSession() - if ok { - b.sessionID = sessionID - } - return ok -} - -func (b *nativeBackend) DestroySession() { - if b.sessionID != 0 { - EndSession(b.sessionID) - b.sessionID = 0 - } -} - -func (b *nativeBackend) ClearComposition() { - if b.sessionID != 0 { - ClearComposition(b.sessionID) - } -} - -func (b *nativeBackend) ProcessKey(req *pime.Request, translatedKeyCode, modifiers int) bool { - if !b.EnsureSession() { - return false - } - return ProcessKey(b.sessionID, translatedKeyCode, modifiers) -} - -func (b *nativeBackend) State() rimeState { - state := rimeState{} - if b.sessionID == 0 { - return state - } - if commit, ok := GetCommit(b.sessionID); ok { - state.CommitString = commit.Text - } - if composition, ok := GetComposition(b.sessionID); ok { - state.Composition = composition.Preedit - state.CursorPos = composition.CursorPos - state.SelStart = composition.SelStart - state.SelEnd = composition.SelEnd - } - if menu, ok := GetMenu(b.sessionID); ok { - candidates := make([]candidateItem, 0, len(menu.Candidates)) - for _, candidate := range menu.Candidates { - candidates = append(candidates, candidateItem{ - Text: candidate.Text, - Comment: candidate.Comment, - }) - } - state.Candidates = candidates - state.CandidateCursor = menu.HighlightedCandidateIndex - state.SelectKeys = menu.SelectKeys - } - state.AsciiMode = b.GetOption("ascii_mode") - state.FullShape = b.GetOption("full_shape") - return state -} - -func (b *nativeBackend) SetOption(name string, value bool) { - if b.EnsureSession() { - SetOption(b.sessionID, name, value) - } -} - -func (b *nativeBackend) GetOption(name string) bool { - if !b.EnsureSession() { - return false - } - return GetOption(b.sessionID, name) -} diff --git a/go-backend/input_methods/rime/native_stub.go b/go-backend/input_methods/rime/native_stub.go deleted file mode 100644 index 82de424a5..000000000 --- a/go-backend/input_methods/rime/native_stub.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build !windows - -package rime - -func newNativeBackend() rimeBackend { - return nil -} diff --git a/go-backend/input_methods/rime/rime.dll b/go-backend/input_methods/rime/rime.dll deleted file mode 100644 index eb86a9d35..000000000 Binary files a/go-backend/input_methods/rime/rime.dll and /dev/null differ diff --git a/go-backend/input_methods/rime/rime.go b/go-backend/input_methods/rime/rime.go deleted file mode 100644 index 8b61b0699..000000000 --- a/go-backend/input_methods/rime/rime.go +++ /dev/null @@ -1,701 +0,0 @@ -// RIME 输入法 Go 实现 -// 对齐 python/input_methods/rime/rime_ime.py -package rime - -import ( - "log" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/EasyIME/pime-go/pime" -) - -const ( - APP = "PIME" - APP_VERSION = "0.01" - - ID_MODE_ICON = 1 - ID_ASCII_MODE = 2 - ID_FULL_SHAPE = 3 - ID_ASCII_PUNCT = 4 - ID_TRADITIONALIZATION = 5 - ID_DEPLOY = 10 - ID_SYNC = 11 - ID_SYNC_DIR = 12 - ID_SHARED_DIR = 13 - ID_USER_DIR = 14 - ID_LOG_DIR = 16 -) - -type Style struct { - DisplayTrayIcon bool - CandidateFormat string - CandidatePerRow int - CandidateUseCursor bool - FontFace string - FontPoint int - InlinePreedit string - SoftCursor bool -} - -type candidateItem struct { - Text string - Comment string -} - -type rimeState struct { - CommitString string - Composition string - CursorPos int - SelStart int - SelEnd int - Candidates []candidateItem - CandidateCursor int - SelectKeys string - AsciiMode bool - FullShape bool -} - -type rimeBackend interface { - Initialize(sharedDir, userDir string, firstRun bool) bool - EnsureSession() bool - DestroySession() - ClearComposition() - ProcessKey(req *pime.Request, translatedKeyCode, modifiers int) bool - State() rimeState - SetOption(name string, value bool) - GetOption(name string) bool -} - -type IME struct { - *pime.TextServiceBase - iconDir string - style Style - selectKeys string - lastKeyDownCode int - lastKeySkip int - lastKeyDownRet bool - lastKeyUpCode int - lastKeyUpRet bool - keyComposing bool - backend rimeBackend -} - -func New(client *pime.Client) pime.TextService { - return &IME{ - TextServiceBase: pime.NewTextServiceBase(client), - style: Style{ - DisplayTrayIcon: true, - CandidateFormat: "{0} {1}", - CandidatePerRow: 1, - CandidateUseCursor: false, - FontFace: "MingLiu", - FontPoint: 20, - InlinePreedit: "composition", - SoftCursor: false, - }, - } -} - -func (ime *IME) HandleRequest(req *pime.Request) *pime.Response { - resp := pime.NewResponse(req.SeqNum, true) - - switch req.Method { - case "onActivate": - return ime.onActivate(req, resp) - case "onDeactivate": - return ime.onDeactivate(req, resp) - case "filterKeyDown": - return ime.filterKeyDown(req, resp) - case "onKeyDown": - return ime.onKeyDown(req, resp) - case "filterKeyUp": - return ime.filterKeyUp(req, resp) - case "onKeyUp": - return ime.onKeyUp(req, resp) - case "onCompositionTerminated": - return ime.onCompositionTerminated(req, resp) - case "onCommand": - return ime.onCommand(req, resp) - case "onMenu": - return ime.onMenu(req, resp) - default: - resp.ReturnValue = 0 - return resp - } -} - -func (ime *IME) onActivate(req *pime.Request, resp *pime.Response) *pime.Response { - log.Println("RIME 输入法已激活") - ime.createSession(resp) - ime.addButtons(resp) - ime.updateLangStatus(req, resp) - resp.ReturnValue = 1 - return resp -} - -func (ime *IME) onDeactivate(req *pime.Request, resp *pime.Response) *pime.Response { - log.Println("RIME 输入法已失活") - ime.destroySession(resp) - ime.removeButtons(resp) - resp.ReturnValue = 1 - return resp -} - -func (ime *IME) filterKeyDown(req *pime.Request, resp *pime.Response) *pime.Response { - if ime.lastKeyDownCode == req.KeyCode { - ime.lastKeySkip++ - if ime.lastKeySkip >= 2 { - ime.lastKeyDownCode = 0 - ime.lastKeySkip = 0 - } - } else { - ime.lastKeyDownCode = req.KeyCode - ime.lastKeySkip = 0 - ime.lastKeyDownRet = ime.processKey(req, false) - } - ime.lastKeyUpCode = 0 - resp.ReturnValue = boolToInt(ime.lastKeyDownRet) - return resp -} - -func (ime *IME) filterKeyUp(req *pime.Request, resp *pime.Response) *pime.Response { - if ime.lastKeyUpCode == req.KeyCode { - ime.lastKeyUpCode = 0 - } else { - ime.lastKeyUpCode = req.KeyCode - ime.lastKeyUpRet = ime.processKey(req, true) - } - ime.lastKeyDownCode = 0 - ime.lastKeySkip = 0 - resp.ReturnValue = boolToInt(ime.lastKeyUpRet) - return resp -} - -func (ime *IME) onKeyDown(req *pime.Request, resp *pime.Response) *pime.Response { - if ime.shouldPassThroughModifierOnKey(req, ime.lastKeyDownRet) { - resp.ReturnValue = 0 - return resp - } - resp.ReturnValue = boolToInt(ime.onKey(req, resp)) - return resp -} - -func (ime *IME) onKeyUp(req *pime.Request, resp *pime.Response) *pime.Response { - if ime.shouldPassThroughModifierOnKey(req, ime.lastKeyUpRet) { - resp.ReturnValue = 0 - return resp - } - resp.ReturnValue = boolToInt(ime.onKey(req, resp)) - return resp -} - -func (ime *IME) onCompositionTerminated(req *pime.Request, resp *pime.Response) *pime.Response { - if req.Forced { - ime.destroySession(resp) - } else if ime.backend != nil { - ime.backend.ClearComposition() - ime.clearResponse(resp) - } - resp.ReturnValue = 1 - return resp -} - -func (ime *IME) onCommand(req *pime.Request, resp *pime.Response) *pime.Response { - commandID := req.ID.IntValue() - if commandID == 0 && req.Data != nil { - if raw, ok := req.Data["commandId"].(float64); ok { - commandID = int(raw) - } - } - if commandID == 0 { - resp.ReturnValue = 0 - return resp - } - - ime.createSession(resp) - - switch commandID { - case ID_ASCII_MODE, ID_MODE_ICON: - ime.toggleOption("ascii_mode") - case ID_FULL_SHAPE: - ime.toggleOption("full_shape") - case ID_ASCII_PUNCT: - ime.toggleOption("ascii_punct") - case ID_TRADITIONALIZATION: - ime.toggleOption("traditionalization") - case ID_DEPLOY: - log.Println("重新部署尚未实现") - case ID_SYNC: - log.Println("同步用户数据尚未实现") - case ID_USER_DIR: - ime.openPath(ime.userDir()) - case ID_SHARED_DIR: - ime.openPath(ime.sharedDir()) - case ID_SYNC_DIR: - ime.openPath(filepath.Join(ime.userDir(), "sync")) - case ID_LOG_DIR: - ime.openPath(filepath.Join(os.Getenv("LOCALAPPDATA"), "PIME", "Logs")) - default: - log.Printf("未知命令: %d", commandID) - resp.ReturnValue = 0 - return resp - } - - ime.updateLangStatus(req, resp) - resp.ReturnValue = 1 - return resp -} - -func (ime *IME) onMenu(req *pime.Request, resp *pime.Response) *pime.Response { - buttonID := req.ID.StringValue() - if buttonID == "" && req.Data != nil { - if raw, ok := req.Data["buttonId"].(string); ok { - buttonID = raw - } else if raw, ok := req.Data["id"].(string); ok { - buttonID = raw - } - } - if buttonID != "settings" && buttonID != "windows-mode-icon" { - resp.ReturnData = []map[string]interface{}{} - resp.ReturnValue = 0 - return resp - } - - resp.ReturnData = ime.buildMenu() - resp.ReturnValue = 1 - return resp -} - -func (ime *IME) Init(req *pime.Request) bool { - log.Println("RIME 输入法初始化") - exePath, err := os.Executable() - if err != nil { - log.Printf("获取可执行文件路径失败,原生 RIME 不可用: %v", err) - return true - } - - exeDir := filepath.Dir(exePath) - ime.iconDir = filepath.Join(exeDir, "input_methods", "rime", "icons") - // After installation this resolves to C:\Program Files (x86)\PIME\go-backend\input_methods\rime\data. - sharedDir := filepath.Join(exeDir, "input_methods", "rime", "data") - - appData := os.Getenv("APPDATA") - if appData == "" { - log.Println("未找到 APPDATA,原生 RIME 不可用") - return true - } - userDir := filepath.Join(appData, APP, "Rime") - info, statErr := os.Stat(userDir) - if statErr != nil || !info.IsDir() { - log.Println("未找到用户 RIME 数据目录,原生 RIME 不可用") - return true - } - - real := newNativeBackend() - if real != nil && real.Initialize(sharedDir, userDir, false) { - ime.backend = real - } else { - ime.backend = nil - } - return true -} - -func (ime *IME) Close() { - ime.destroySession(nil) - log.Println("RIME 输入法关闭") -} - -func (ime *IME) BackendAvailable() bool { - return ime.backend != nil -} - -func (ime *IME) processKey(req *pime.Request, isUp bool) bool { - ime.createSession(nil) - if ime.backend == nil { - ime.logShortcutTrace(req, isUp, 0, 0, false, false) - return false - } - if !isUp { - ime.keyComposing = ime.isComposing() - } - translatedKeyCode := translateKeyCode(req) - modifiers := translateModifiers(req, isUp) - backendRet := ime.backend.ProcessKey(req, translatedKeyCode, modifiers) - handled := backendRet - if backendRet { - ime.logShortcutTrace(req, isUp, translatedKeyCode, modifiers, backendRet, true) - return true - } - if ime.keyComposing && req.KeyCode == vkReturn { - handled = true - ime.logShortcutTrace(req, isUp, translatedKeyCode, modifiers, backendRet, handled) - return true - } - if (req.KeyCode == vkShift || req.KeyCode == vkCapital) && - (modifiers == 0 || modifiers == releaseMask) { - handled = true - ime.logShortcutTrace(req, isUp, translatedKeyCode, modifiers, backendRet, handled) - return true - } - ime.logShortcutTrace(req, isUp, translatedKeyCode, modifiers, backendRet, handled) - return false -} - -func (ime *IME) logShortcutTrace(req *pime.Request, isUp bool, translatedKeyCode, modifiers int, backendRet, handled bool) { - if req == nil { - return - } - if modifiers&controlMask == 0 && modifiers&altMask == 0 && req.KeyCode != vkControl && req.KeyCode != vkMenu { - return - } - - eventType := "down" - if isUp { - eventType = "up" - } - log.Printf( - "RIME 快捷键追踪 event=%s keyCode=%d charCode=%d translatedKey=%d modifiers=%d ctrl=%t alt=%t backendRet=%t handled=%t composing=%t", - eventType, - req.KeyCode, - req.CharCode, - translatedKeyCode, - modifiers, - (modifiers&controlMask) != 0 || req.KeyCode == vkControl, - (modifiers&altMask) != 0 || req.KeyCode == vkMenu, - backendRet, - handled, - ime.keyComposing, - ) -} - -func (ime *IME) shouldPassThroughModifierOnKey(req *pime.Request, filterHandled bool) bool { - if req == nil || filterHandled { - return false - } - if req.KeyCode == vkControl || req.KeyCode == vkMenu { - return true - } - if req.CharCode > 0 && req.CharCode < 0x20 { - return true - } - return req.KeyStates.IsKeyDown(vkControl) || req.KeyStates.IsKeyDown(vkMenu) -} - -func (ime *IME) onKey(req *pime.Request, resp *pime.Response) bool { - if ime.backend == nil { - ime.clearResponse(resp) - ime.keyComposing = false - return true - } - ime.updateLangStatus(req, resp) - state := ime.backend.State() - if state.CommitString != "" { - resp.CommitString = state.CommitString - } - if state.Composition == "" { - ime.clearResponse(resp) - ime.keyComposing = false - return true - } - - if state.SelectKeys != "" && state.SelectKeys != ime.selectKeys { - resp.SetSelKeys = state.SelectKeys - ime.selectKeys = state.SelectKeys - } - - resp.CompositionString = state.Composition - resp.CursorPos = state.CursorPos - resp.CompositionCursor = state.CursorPos - resp.SelStart = state.SelStart - resp.SelEnd = state.SelEnd - - if len(state.Candidates) > 0 { - resp.CandidateList = ime.formatCandidates(state.Candidates) - resp.CandidateCursor = state.CandidateCursor - resp.ShowCandidates = true - } else { - resp.ShowCandidates = false - } - ime.keyComposing = true - return true -} - -func (ime *IME) createSession(resp *pime.Response) { - if ime.backend == nil { - return - } - if !ime.backend.EnsureSession() { - return - } - if resp != nil { - resp.CustomizeUI = map[string]interface{}{ - "candFontName": ime.style.FontFace, - "candFontSize": ime.style.FontPoint, - "candPerRow": ime.style.CandidatePerRow, - "candUseCursor": ime.style.CandidateUseCursor, - } - } -} - -func (ime *IME) destroySession(resp *pime.Response) { - ime.clearResponse(resp) - if ime.backend != nil { - ime.backend.ClearComposition() - ime.backend.DestroySession() - } - ime.keyComposing = false - ime.selectKeys = "" -} - -func (ime *IME) clearResponse(resp *pime.Response) { - if resp == nil { - return - } - resp.CompositionString = "" - resp.CursorPos = 0 - resp.CompositionCursor = 0 - resp.CandidateList = []string{} - resp.CandidateCursor = 0 - resp.ShowCandidates = false -} - -func (ime *IME) isComposing() bool { - if ime.backend == nil { - return false - } - state := ime.backend.State() - return state.Composition != "" || len(state.Candidates) > 0 -} - -func (ime *IME) toggleOption(name string) { - if ime.backend == nil { - return - } - ime.backend.SetOption(name, !ime.backend.GetOption(name)) -} - -func (ime *IME) updateLangStatus(req *pime.Request, resp *pime.Response) { - if !ime.style.DisplayTrayIcon || ime.backend == nil { - return - } - asciiMode := ime.backend.GetOption("ascii_mode") - fullShape := ime.backend.GetOption("full_shape") - capsOn := req != nil && req.KeyStates.IsKeyToggled(vkCapital) - - if ime.Client != nil && ime.Client.IsWindows8Above { - if iconPath := ime.iconPath(modeIconName(asciiMode, fullShape, capsOn)); iconPath != "" { - resp.ChangeButton = append(resp.ChangeButton, pime.ButtonInfo{ - ID: "windows-mode-icon", - Icon: iconPath, - }) - } - } - if iconPath := ime.iconPath(langIconName(asciiMode)); iconPath != "" { - resp.ChangeButton = append(resp.ChangeButton, pime.ButtonInfo{ - ID: "switch-lang", - Icon: iconPath, - }) - } - if iconPath := ime.iconPath(shapeIconName(fullShape)); iconPath != "" { - resp.ChangeButton = append(resp.ChangeButton, pime.ButtonInfo{ - ID: "switch-shape", - Icon: iconPath, - }) - } -} - -func (ime *IME) addButtons(resp *pime.Response) { - if !ime.style.DisplayTrayIcon || ime.backend == nil { - return - } - asciiMode := ime.backend.GetOption("ascii_mode") - fullShape := ime.backend.GetOption("full_shape") - if ime.Client != nil && ime.Client.IsWindows8Above { - if iconPath := ime.iconPath(modeIconName(asciiMode, fullShape, false)); iconPath != "" { - resp.AddButton = append(resp.AddButton, pime.ButtonInfo{ - ID: "windows-mode-icon", - Icon: iconPath, - Tooltip: "中西文切换", - CommandID: ID_MODE_ICON, - }) - } - } - if iconPath := ime.iconPath(langIconName(asciiMode)); iconPath != "" { - resp.AddButton = append(resp.AddButton, pime.ButtonInfo{ - ID: "switch-lang", - Icon: iconPath, - Text: "中西文切换", - Tooltip: "中西文切换", - CommandID: ID_ASCII_MODE, - }) - } - if iconPath := ime.iconPath(shapeIconName(fullShape)); iconPath != "" { - resp.AddButton = append(resp.AddButton, pime.ButtonInfo{ - ID: "switch-shape", - Icon: iconPath, - Text: "全半角切换", - Tooltip: "全角/半角切换", - CommandID: ID_FULL_SHAPE, - }) - } - if iconPath := ime.iconPath("config.ico"); iconPath != "" { - resp.AddButton = append(resp.AddButton, pime.ButtonInfo{ - ID: "settings", - Icon: iconPath, - Text: "设置", - Type: "menu", - }) - } -} - -func (ime *IME) removeButtons(resp *pime.Response) { - if !ime.style.DisplayTrayIcon || resp == nil { - return - } - resp.RemoveButton = append(resp.RemoveButton, "switch-lang", "switch-shape", "settings") - if ime.Client != nil && ime.Client.IsWindows8Above { - resp.RemoveButton = append(resp.RemoveButton, "windows-mode-icon") - } -} - -func (ime *IME) formatCandidates(candidates []candidateItem) []string { - formatted := make([]string, 0, len(candidates)) - for _, candidate := range candidates { - text := candidate.Text - if candidate.Comment != "" { - if strings.Contains(ime.style.CandidateFormat, "{0}") && strings.Contains(ime.style.CandidateFormat, "{1}") { - text = strings.ReplaceAll(ime.style.CandidateFormat, "{0}", candidate.Text) - text = strings.ReplaceAll(text, "{1}", candidate.Comment) - } else { - text = candidate.Text + " " + candidate.Comment - } - } - formatted = append(formatted, text) - } - return formatted -} - -func (ime *IME) iconPath(name string) string { - if ime.iconDir == "" || name == "" { - return "" - } - iconPath := filepath.Join(ime.iconDir, name) - if _, err := os.Stat(iconPath); err != nil { - return "" - } - return iconPath -} - -func (ime *IME) buildMenu() []map[string]interface{} { - asciiMode := ime.backend != nil && ime.backend.GetOption("ascii_mode") - fullShape := ime.backend != nil && ime.backend.GetOption("full_shape") - asciiPunct := ime.backend != nil && ime.backend.GetOption("ascii_punct") - traditionalization := ime.backend != nil && ime.backend.GetOption("traditionalization") - - asciiText := "中文 → 英文" - if asciiMode { - asciiText = "英文 → 中文" - } - shapeText := "半角 → 全角" - if fullShape { - shapeText = "全角 → 半角" - } - punctText := "中文标点 → 英文标点" - if asciiPunct { - punctText = "英文标点 → 中文标点" - } - traditionalizationText := "简体 → 繁体" - if traditionalization { - traditionalizationText = "繁体 → 简体" - } - - return []map[string]interface{}{ - {"id": ID_ASCII_MODE, "text": asciiText}, - {"id": ID_TRADITIONALIZATION, "text": traditionalizationText}, - {"id": ID_ASCII_PUNCT, "text": punctText}, - {"id": ID_FULL_SHAPE, "text": shapeText}, - {"text": ""}, - {"id": ID_DEPLOY, "text": "重新部署(&D)"}, - {"id": ID_SYNC, "text": "同步(&S)"}, - {"text": "打开文件夹(&O)", "submenu": []map[string]interface{}{ - {"id": ID_USER_DIR, "text": "用户文件夹"}, - {"id": ID_SHARED_DIR, "text": "共享文件夹"}, - {"id": ID_SYNC_DIR, "text": "同步文件夹"}, - {"id": ID_LOG_DIR, "text": "日志文件夹"}, - }}, - } -} - -func (ime *IME) sharedDir() string { - exePath, err := os.Executable() - if err != nil { - return "" - } - return filepath.Join(filepath.Dir(exePath), "input_methods", "rime", "data") -} - -func (ime *IME) userDir() string { - appData := os.Getenv("APPDATA") - if appData == "" { - return "" - } - return filepath.Join(appData, APP, "Rime") -} - -func (ime *IME) openPath(path string) { - if path == "" { - return - } - if err := exec.Command("explorer.exe", path).Start(); err != nil { - log.Printf("打开路径失败 %s: %v", path, err) - } -} - -func (ime *IME) openURL(rawURL string) { - if rawURL == "" { - return - } - if err := exec.Command("rundll32.exe", "url.dll,FileProtocolHandler", rawURL).Start(); err != nil { - log.Printf("打开链接失败 %s: %v", rawURL, err) - } -} - -func modeIconName(asciiMode, fullShape, capsOn bool) string { - lang := "chi" - if asciiMode { - lang = "eng" - } - shape := "half" - if fullShape { - shape = "full" - } - caps := "off" - if capsOn { - caps = "on" - } - return lang + "_" + shape + "_caps" + caps + ".ico" -} - -func langIconName(asciiMode bool) string { - if asciiMode { - return "eng.ico" - } - return "chi.ico" -} - -func shapeIconName(fullShape bool) string { - if fullShape { - return "full.ico" - } - return "half.ico" -} - -func boolToInt(v bool) int { - if v { - return 1 - } - return 0 -} diff --git a/go-backend/input_methods/rime/rime_keyevent.go b/go-backend/input_methods/rime/rime_keyevent.go deleted file mode 100644 index f2876445b..000000000 --- a/go-backend/input_methods/rime/rime_keyevent.go +++ /dev/null @@ -1,171 +0,0 @@ -package rime - -import "github.com/EasyIME/pime-go/pime" - -const ( - voidSymbol = 0xFFFFFF - - rimeSpace = 0x020 - rimeBackSpace = 0xFF08 - rimeTab = 0xFF09 - rimeClear = 0xFF0B - rimeReturn = 0xFF0D - rimePause = 0xFF13 - rimeCapsLock = 0xFFE5 - rimeEscape = 0xFF1B - rimePrior = 0xFF55 - rimeNext = 0xFF56 - rimeEnd = 0xFF57 - rimeHome = 0xFF50 - rimeLeft = 0xFF51 - rimeUp = 0xFF52 - rimeRight = 0xFF53 - rimeDown = 0xFF54 - rimeSelect = 0xFF60 - rimePrint = 0xFF61 - rimeExecute = 0xFF62 - rimeInsert = 0xFF63 - rimeDelete = 0xFFFF - rimeHelp = 0xFF6A - rimeMetaL = 0xFFE7 - rimeMetaR = 0xFFE8 - rimeNumLock = 0xFF7F - rimeScrollLock = 0xFF14 - rimeShiftL = 0xFFE1 - rimeShiftR = 0xFFE2 - rimeControlL = 0xFFE3 - rimeControlR = 0xFFE4 - rimeAltL = 0xFFE9 - rimeAltR = 0xFFEA - - shiftMask = 1 << 0 - lockMask = 1 << 1 - controlMask = 1 << 2 - altMask = 1 << 3 - releaseMask = 1 << 30 - - vkBack = 0x08 - vkTab = 0x09 - vkClear = 0x0C - vkReturn = 0x0D - vkShift = 0x10 - vkControl = 0x11 - vkMenu = 0x12 - vkPause = 0x13 - vkCapital = 0x14 - vkEscape = 0x1B - vkSpace = 0x20 - vkPrior = 0x21 - vkNext = 0x22 - vkEnd = 0x23 - vkHome = 0x24 - vkLeft = 0x25 - vkUp = 0x26 - vkRight = 0x27 - vkDown = 0x28 - vkSelect = 0x29 - vkPrint = 0x2A - vkExecute = 0x2B - vkInsert = 0x2D - vkDelete = 0x2E - vkHelp = 0x2F - vkLWin = 0x5B - vkRWin = 0x5C - vkNumLock = 0x90 - vkScroll = 0x91 - vkLShift = 0xA0 - vkRShift = 0xA1 - vkLControl = 0xA2 - vkRControl = 0xA3 - vkLMenu = 0xA4 - vkRMenu = 0xA5 -) - -var vkMaps = map[int]int{ - vkBack: rimeBackSpace, - vkTab: rimeTab, - vkClear: rimeClear, - vkReturn: rimeReturn, - vkMenu: rimeAltL, - vkPause: rimePause, - vkCapital: rimeCapsLock, - vkEscape: rimeEscape, - vkSpace: rimeSpace, - vkPrior: rimePrior, - vkNext: rimeNext, - vkEnd: rimeEnd, - vkHome: rimeHome, - vkLeft: rimeLeft, - vkUp: rimeUp, - vkRight: rimeRight, - vkDown: rimeDown, - vkSelect: rimeSelect, - vkPrint: rimePrint, - vkExecute: rimeExecute, - vkInsert: rimeInsert, - vkDelete: rimeDelete, - vkHelp: rimeHelp, - vkLWin: rimeMetaL, - vkRWin: rimeMetaR, - vkNumLock: rimeNumLock, - vkScroll: rimeScrollLock, - vkLShift: rimeShiftL, - vkRShift: rimeShiftR, - vkLControl: rimeControlL, - vkRControl: rimeControlR, - vkLMenu: rimeAltL, - vkRMenu: rimeAltR, -} - -func translateKeyCode(req *pime.Request) int { - keyCode := req.KeyCode - if keyCode == vkShift { - if req.KeyStates.IsKeyToggled(vkRShift) { - keyCode = vkRShift - } else { - keyCode = vkLShift - } - } else if keyCode == vkControl { - if req.KeyStates.IsKeyToggled(vkRControl) { - keyCode = vkRControl - } else { - keyCode = vkLControl - } - } - - if mapped, ok := vkMaps[keyCode]; ok { - return mapped - } - if isPrintableChar(req) { - return req.CharCode - } - return voidSymbol -} - -func translateModifiers(req *pime.Request, isUp bool) int { - result := 0 - keyCode := req.KeyCode - if keyCode != vkShift && req.KeyStates.IsKeyDown(vkShift) { - result |= shiftMask - } - if req.KeyStates.IsKeyToggled(vkCapital) { - result |= lockMask - } - if keyCode != vkControl && req.KeyStates.IsKeyDown(vkControl) { - result |= controlMask - } - if keyCode != vkMenu && req.KeyStates.IsKeyDown(vkMenu) { - result |= altMask - } - if isUp { - result |= releaseMask - } - if keyCode == vkCapital && !isUp { - result ^= lockMask - } - return result -} - -func isPrintableChar(req *pime.Request) bool { - return req.CharCode > 0x1f && req.CharCode != 0x7f -} diff --git a/go-backend/input_methods/rime/rime_runtime_test.go b/go-backend/input_methods/rime/rime_runtime_test.go deleted file mode 100644 index df685f9d3..000000000 --- a/go-backend/input_methods/rime/rime_runtime_test.go +++ /dev/null @@ -1,281 +0,0 @@ -//go:build windows - -package rime - -import ( - "os" - "path/filepath" - "strings" - "testing" - - "github.com/EasyIME/pime-go/pime" -) - -func newRealRimeSession(t *testing.T) RimeSessionId { - t.Helper() - - wd := "D:\\vscode\\moqi-input-method-projs\\PIME\\go-backend\\input_methods\\rime" - appData := os.Getenv("APPDATA") - if appData == "" { - t.Skip("APPDATA is not set") - } - userDir := filepath.Join(appData, APP, "Rime") - if info, err := os.Stat(userDir); err != nil || !info.IsDir() { - t.Skip("existing user Rime directory is required") - } - dataDir := filepath.Join(wd, "data") - - if !RimeInit(dataDir, userDir, APP, APP_VERSION, false) { - t.Fatal("RimeInit failed") - } - - sessionID, ok := StartSession() - if !ok || sessionID == 0 { - t.Fatal("StartSession failed") - } - t.Cleanup(func() { - EndSession(sessionID) - Finalize() - }) - t.Logf("ascii_mode before typing: %t", GetOption(sessionID, "ascii_mode")) - t.Logf("full_shape before typing: %t", GetOption(sessionID, "full_shape")) - SetOption(sessionID, "ascii_mode", false) - t.Logf("ascii_mode after forcing off: %t", GetOption(sessionID, "ascii_mode")) - return sessionID -} - -func TestRealRimeCanCommitText(t *testing.T) { - sessionID := newRealRimeSession(t) - - for _, input := range []string{"nihao", "xpxp", "gegegojxyzgegegojxdegoge"} { - t.Run(input, func(t *testing.T) { - ClearComposition(sessionID) - for _, key := range []rune(input) { - if !ProcessKey(sessionID, int(key), 0) { - if composition, ok := GetComposition(sessionID); ok { - t.Logf("composition after failed %q: %#v", key, composition) - } - if menu, ok := GetMenu(sessionID); ok { - t.Logf("menu after failed %q: %#v", key, menu) - } - t.Fatalf("ProcessKey failed for %q", key) - } - } - - menu, ok := GetMenu(sessionID) - if !ok || len(menu.Candidates) == 0 { - t.Fatalf("expected candidates after %s, got %#v", input, menu) - } - t.Logf("candidates after %s: %#v", input, menu.Candidates) - - if !ProcessKey(sessionID, int(' '), 0) { - t.Fatal("ProcessKey failed for space") - } - - commit, ok := GetCommit(sessionID) - if !ok { - t.Fatal("expected commit after space") - } - t.Logf("commit text for %s: %q", input, commit.Text) - - if commit.Text == "" || commit.Text == input { - t.Fatalf("expected converted text commit for %s, got %q", input, commit.Text) - } - }) - } -} - -func TestRealRimeControlShortcuts(t *testing.T) { - sessionID := newRealRimeSession(t) - - tests := []struct { - name string - req *pime.Request - }{ - { - name: "ctrl+a", - req: &pime.Request{ - KeyCode: 'A', - CharCode: 1, - KeyStates: keyStatesDown(vkControl), - }, - }, - { - name: "ctrl+grave", - req: &pime.Request{ - KeyCode: 0xC0, - CharCode: '`', - KeyStates: keyStatesDown(vkControl), - }, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - ClearComposition(sessionID) - - translatedKey := translateKeyCode(tc.req) - modifiers := translateModifiers(tc.req, false) - handled := ProcessKey(sessionID, translatedKey, modifiers) - - t.Logf("request: keyCode=%d charCode=%d translatedKey=%d modifiers=%d handled=%t", - tc.req.KeyCode, tc.req.CharCode, translatedKey, modifiers, handled) - - if composition, ok := GetComposition(sessionID); ok { - t.Logf("composition: %#v", composition) - } else { - t.Log("composition: ") - } - - if menu, ok := GetMenu(sessionID); ok { - t.Logf("menu: %#v", menu) - } else { - t.Log("menu: ") - } - - if commit, ok := GetCommit(sessionID); ok { - t.Logf("commit: %#v", commit) - } else { - t.Log("commit: ") - } - }) - } -} - -func TestRealRimeBackspaceUpdatesComposition(t *testing.T) { - sessionID := newRealRimeSession(t) - ClearComposition(sessionID) - - typeASCII(t, sessionID, "ni") - before, ok := GetComposition(sessionID) - if !ok || before.Preedit == "" { - t.Fatalf("expected composition before backspace, got %#v", before) - } - - handled := processRealKey(sessionID, &pime.Request{KeyCode: vkBack}) - after, ok := GetComposition(sessionID) - if !handled { - t.Fatal("expected backspace to be handled") - } - if !ok || after.Preedit == "" { - t.Fatalf("expected composition to remain after backspace, got %#v", after) - } - if len([]rune(after.Preedit)) >= len([]rune(before.Preedit)) { - t.Fatalf("expected shorter composition after backspace, before=%q after=%q", before.Preedit, after.Preedit) - } - if menu, ok := GetMenu(sessionID); !ok || len(menu.Candidates) == 0 { - t.Fatalf("expected candidates to remain after backspace, got %#v", menu) - } -} - -func TestRealRimeEscapeClearsComposition(t *testing.T) { - sessionID := newRealRimeSession(t) - ClearComposition(sessionID) - - typeASCII(t, sessionID, "ni") - if composition, ok := GetComposition(sessionID); !ok || composition.Preedit == "" { - t.Fatalf("expected composition before escape, got %#v", composition) - } - - handled := processRealKey(sessionID, &pime.Request{KeyCode: vkEscape}) - composition, compositionOK := GetComposition(sessionID) - menu, menuOK := GetMenu(sessionID) - if !handled { - t.Fatal("expected escape to be handled") - } - if !compositionOK || composition.Preedit != "" { - t.Fatalf("expected escape to clear composition, got %#v", composition) - } - if menuOK && len(menu.Candidates) != 0 { - t.Fatalf("expected escape to clear candidates, got %#v", menu) - } -} - -func TestRealRimePunctuationKeys(t *testing.T) { - sessionID := newRealRimeSession(t) - - tests := []struct { - name string - req *pime.Request - allowedCommit []string - }{ - { - name: "grave", - req: &pime.Request{ - KeyCode: 0xC0, - CharCode: '`', - }, - allowedCommit: []string{"、", "`", "`"}, - }, - { - name: "pipe", - req: &pime.Request{ - KeyCode: 0xDC, - CharCode: '|', - }, - allowedCommit: []string{"|", "·", "|"}, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - ClearComposition(sessionID) - - handled := processRealKey(sessionID, tc.req) - commit, commitOK := GetCommit(sessionID) - composition, compositionOK := GetComposition(sessionID) - menu, menuOK := GetMenu(sessionID) - - t.Logf("request=%s handled=%t commit=%#v composition=%#v menu=%#v", tc.name, handled, commit, composition, menu) - - if !handled { - t.Fatalf("expected %s key to be handled", tc.name) - } - if commitOK && commit.Text != "" { - if !containsAny(tc.allowedCommit, commit.Text) { - t.Fatalf("unexpected commit for %s: %q", tc.name, commit.Text) - } - return - } - if compositionOK && composition.Preedit != "" { - return - } - if menuOK && len(menu.Candidates) > 0 { - return - } - t.Fatalf("expected %s key to produce visible output", tc.name) - }) - } -} - -func keyStatesDown(codes ...int) pime.KeyStates { - states := make(pime.KeyStates, 256) - for _, code := range codes { - if code >= 0 && code < len(states) { - states[code] = 1 << 7 - } - } - return states -} - -func processRealKey(sessionID RimeSessionId, req *pime.Request) bool { - return ProcessKey(sessionID, translateKeyCode(req), translateModifiers(req, false)) -} - -func typeASCII(t *testing.T, sessionID RimeSessionId, input string) { - t.Helper() - for _, key := range input { - if !ProcessKey(sessionID, int(key), 0) { - t.Fatalf("ProcessKey failed for %q", key) - } - } -} - -func containsAny(candidates []string, got string) bool { - for _, candidate := range candidates { - if strings.Contains(got, candidate) { - return true - } - } - return false -} diff --git a/go-backend/input_methods/rime/rime_test.go b/go-backend/input_methods/rime/rime_test.go deleted file mode 100644 index dd4f1dad9..000000000 --- a/go-backend/input_methods/rime/rime_test.go +++ /dev/null @@ -1,632 +0,0 @@ -package rime - -import ( - "strings" - "testing" - - "github.com/EasyIME/pime-go/pime" -) - -type testDictEntry struct { - code string - words []candidateItem -} - -type testBackend struct { - session bool - composition string - candidates []candidateItem - commitString string - asciiMode bool - fullShape bool -} - -func newTestBackend() *testBackend { - return &testBackend{} -} - -func (b *testBackend) Initialize(sharedDir, userDir string, firstRun bool) bool { - return true -} - -func (b *testBackend) EnsureSession() bool { - b.session = true - return true -} - -func (b *testBackend) DestroySession() { - b.session = false - b.ClearComposition() -} - -func (b *testBackend) ClearComposition() { - b.composition = "" - b.candidates = nil - b.commitString = "" -} - -func (b *testBackend) ProcessKey(req *pime.Request, translatedKeyCode, modifiers int) bool { - b.commitString = "" - keyCode := req.KeyCode - charCode := req.CharCode - if charCode == 0 && keyCode >= 'A' && keyCode <= 'Z' { - charCode = keyCode + 32 - } - if b.asciiMode && b.composition == "" && charCode >= 0x20 { - return false - } - if modifiers&releaseMask != 0 { - return false - } - - switch keyCode { - case vkBack: - if b.composition == "" { - return false - } - b.composition = trimLastRuneForTest(b.composition) - b.refreshCandidates() - return true - case vkEscape: - if b.composition == "" { - return false - } - b.ClearComposition() - return true - case vkReturn, vkSpace: - if b.composition == "" { - return false - } - b.commitString = b.currentCommit() - b.composition = "" - b.candidates = nil - return true - } - - if b.composition != "" && keyCode >= '1' && keyCode <= '9' { - index := keyCode - '1' - if index >= 0 && index < len(b.candidates) { - b.commitString = b.candidates[index].Text - b.composition = "" - b.candidates = nil - return true - } - } - - if charCode >= 'a' && charCode <= 'z' { - b.composition += string(rune(charCode)) - b.refreshCandidates() - return true - } - if charCode == '\'' && b.composition != "" && !strings.HasSuffix(b.composition, "'") { - b.composition += "'" - b.refreshCandidates() - return true - } - if b.composition != "" && charCode >= 0x20 && charCode != '\'' { - b.commitString = b.currentCommit() + string(rune(charCode)) - b.composition = "" - b.candidates = nil - return true - } - return false -} - -func (b *testBackend) State() rimeState { - state := rimeState{ - CommitString: b.commitString, - Composition: b.composition, - CursorPos: len(b.composition), - Candidates: append([]candidateItem(nil), b.candidates...), - CandidateCursor: 0, - SelectKeys: "1234567890", - AsciiMode: b.asciiMode, - FullShape: b.fullShape, - } - b.commitString = "" - return state -} - -func (b *testBackend) SetOption(name string, value bool) { - switch name { - case "ascii_mode": - b.asciiMode = value - case "full_shape": - b.fullShape = value - } -} - -func (b *testBackend) GetOption(name string) bool { - switch name { - case "ascii_mode": - return b.asciiMode - case "full_shape": - return b.fullShape - default: - return false - } -} - -func (b *testBackend) currentCommit() string { - if len(b.candidates) > 0 { - return b.candidates[0].Text - } - return strings.ReplaceAll(b.composition, "'", "") -} - -func (b *testBackend) refreshCandidates() { - code := strings.ReplaceAll(b.composition, "'", "") - if code == "" { - b.candidates = nil - return - } - results := make([]candidateItem, 0, 9) - seen := make(map[string]struct{}) - appendWords := func(words []candidateItem) { - for _, word := range words { - if _, ok := seen[word.Text]; ok { - continue - } - seen[word.Text] = struct{}{} - results = append(results, word) - if len(results) == 9 { - return - } - } - } - for _, entry := range testDictionary() { - if entry.code == code { - appendWords(entry.words) - } - } - for _, entry := range testDictionary() { - if len(results) == 9 { - break - } - if entry.code != code && strings.HasPrefix(entry.code, code) { - appendWords(entry.words) - } - } - if len(results) == 0 { - results = []candidateItem{{Text: code}} - } - b.candidates = results -} - -func testDictionary() []testDictEntry { - return []testDictEntry{ - {code: "ni", words: []candidateItem{{Text: "你"}, {Text: "呢"}, {Text: "泥"}, {Text: "尼"}, {Text: "拟"}}}, - {code: "nihao", words: []candidateItem{{Text: "你好"}, {Text: "你号"}, {Text: "拟好"}}}, - {code: "nimen", words: []candidateItem{{Text: "你们"}}}, - {code: "zhong", words: []candidateItem{{Text: "中"}, {Text: "种"}, {Text: "重"}}}, - {code: "zhongwen", words: []candidateItem{{Text: "中文"}}}, - } -} - -func trimLastRuneForTest(s string) string { - if s == "" { - return s - } - runes := []rune(s) - return string(runes[:len(runes)-1]) -} - -func newTestIME() *IME { - return &IME{ - TextServiceBase: pime.NewTextServiceBase(&pime.Client{ID: "test-client"}), - style: Style{ - DisplayTrayIcon: true, - CandidateFormat: "{0} {1}", - CandidatePerRow: 1, - CandidateUseCursor: false, - FontFace: "MingLiu", - FontPoint: 20, - InlinePreedit: "composition", - SoftCursor: false, - }, - backend: newTestBackend(), - } -} - -func TestNewInitialState(t *testing.T) { - ime := newTestIME() - backend := ime.backend.(*testBackend) - - if !ime.style.DisplayTrayIcon { - t.Fatal("expected tray icon style enabled by default") - } - if backend.composition != "" { - t.Fatalf("expected empty composition, got %q", backend.composition) - } - if len(backend.candidates) != 0 { - t.Fatalf("expected no candidates, got %v", backend.candidates) - } - if ime.keyComposing { - t.Fatal("expected keyComposing to be false initially") - } -} - -func TestFilterKeyDownProcessesKeyWithoutUpdatingUI(t *testing.T) { - ime := newTestIME() - - resp := ime.filterKeyDown(&pime.Request{ - SeqNum: 1, - KeyCode: 0x4E, - CharCode: 'n', - }, pime.NewResponse(1, true)) - - if resp.ReturnValue != 1 { - t.Fatalf("expected n to be handled, got %d", resp.ReturnValue) - } - if resp.CompositionString != "" || len(resp.CandidateList) != 0 || resp.ShowCandidates { - t.Fatalf("expected filterKeyDown not to emit UI state, got %#v", resp) - } -} - -func TestFilterKeyDownFallsBackToKeyCodeWhenCharCodeMissing(t *testing.T) { - ime := newTestIME() - - resp := ime.filterKeyDown(&pime.Request{ - SeqNum: 2, - KeyCode: 0x4E, - }, pime.NewResponse(2, true)) - - if resp.ReturnValue != 1 { - t.Fatalf("expected keyCode-only N to be handled, got %d", resp.ReturnValue) - } -} - -func TestOnKeyDownReflectsBackendStateAfterFilter(t *testing.T) { - ime := newTestIME() - - ime.filterKeyDown(&pime.Request{ - SeqNum: 1, - KeyCode: 0x4E, - CharCode: 'n', - }, pime.NewResponse(1, true)) - ime.filterKeyDown(&pime.Request{ - SeqNum: 2, - KeyCode: 0x49, - CharCode: 'i', - }, pime.NewResponse(2, true)) - - resp := ime.onKeyDown(&pime.Request{ - SeqNum: 3, - KeyCode: 0x49, - CharCode: 'i', - }, pime.NewResponse(3, true)) - - if resp.ReturnValue != 1 { - t.Fatalf("expected onKeyDown to succeed, got %d", resp.ReturnValue) - } - if resp.CompositionString != "ni" { - t.Fatalf("expected composition ni, got %q", resp.CompositionString) - } - if len(resp.CandidateList) == 0 || resp.CandidateList[0] != "你" { - t.Fatalf("expected first exact candidate 你, got %v", resp.CandidateList) - } -} - -func TestOnKeyDownNumberSelectsCandidate(t *testing.T) { - ime := newTestIME() - backend := ime.backend.(*testBackend) - backend.composition = "ni" - backend.candidates = []candidateItem{{Text: "你"}, {Text: "呢"}, {Text: "泥"}} - ime.keyComposing = true - - filterResp := ime.filterKeyDown(&pime.Request{ - SeqNum: 4, - KeyCode: 0x32, - }, pime.NewResponse(4, true)) - if filterResp.ReturnValue != 1 { - t.Fatalf("expected number selection to be handled, got %d", filterResp.ReturnValue) - } - - resp := ime.onKeyDown(&pime.Request{ - SeqNum: 5, - KeyCode: 0x32, - }, pime.NewResponse(5, true)) - - if resp.ReturnValue != 1 { - t.Fatalf("expected onKeyDown after selection to succeed, got %d", resp.ReturnValue) - } - if resp.CommitString != "呢" { - t.Fatalf("expected second candidate 呢, got %q", resp.CommitString) - } - if backend.composition != "" || backend.candidates != nil { - t.Fatal("expected state reset after candidate selection") - } -} - -func TestOnKeyDownBackspaceUpdatesComposition(t *testing.T) { - ime := newTestIME() - backend := ime.backend.(*testBackend) - backend.composition = "ni" - backend.refreshCandidates() - - ime.filterKeyDown(&pime.Request{ - SeqNum: 5, - KeyCode: 0x08, - }, pime.NewResponse(5, true)) - resp := ime.onKeyDown(&pime.Request{ - SeqNum: 6, - KeyCode: 0x08, - }, pime.NewResponse(6, true)) - - if resp.ReturnValue != 1 { - t.Fatalf("expected backspace to be handled, got %d", resp.ReturnValue) - } - if backend.composition != "n" { - t.Fatalf("expected composition n after backspace, got %q", backend.composition) - } - if resp.CompositionString != "n" { - t.Fatalf("expected response composition n, got %q", resp.CompositionString) - } - if len(resp.CandidateList) == 0 { - t.Fatal("expected candidates to remain after backspace") - } -} - -func TestOnKeyDownEscapeClearsComposition(t *testing.T) { - ime := newTestIME() - backend := ime.backend.(*testBackend) - backend.composition = "ni" - backend.refreshCandidates() - - ime.filterKeyDown(&pime.Request{ - SeqNum: 6, - KeyCode: 0x1B, - }, pime.NewResponse(6, true)) - resp := ime.onKeyDown(&pime.Request{ - SeqNum: 7, - KeyCode: 0x1B, - }, pime.NewResponse(7, true)) - - if resp.ReturnValue != 1 { - t.Fatalf("expected escape to be handled, got %d", resp.ReturnValue) - } - if backend.composition != "" || backend.candidates != nil { - t.Fatal("expected composition state cleared") - } - if resp.CompositionString != "" || resp.ShowCandidates { - t.Fatalf("expected cleared UI, got %#v", resp) - } -} - -func TestOnKeyDownSpaceCommitsFirstCandidate(t *testing.T) { - ime := newTestIME() - backend := ime.backend.(*testBackend) - backend.composition = "ni" - backend.refreshCandidates() - - ime.filterKeyDown(&pime.Request{ - SeqNum: 7, - KeyCode: 0x20, - }, pime.NewResponse(7, true)) - resp := ime.onKeyDown(&pime.Request{ - SeqNum: 8, - KeyCode: 0x20, - }, pime.NewResponse(8, true)) - - if resp.ReturnValue != 1 { - t.Fatalf("expected space to be handled, got %d", resp.ReturnValue) - } - if resp.CommitString != "你" { - t.Fatalf("expected first candidate 你, got %q", resp.CommitString) - } - if backend.composition != "" || backend.candidates != nil { - t.Fatal("expected state reset after commit") - } -} - -func TestOnKeyDownPunctuationCommitsComposition(t *testing.T) { - ime := newTestIME() - backend := ime.backend.(*testBackend) - backend.composition = "ni" - backend.refreshCandidates() - - ime.filterKeyDown(&pime.Request{ - SeqNum: 8, - KeyCode: int('.'), - CharCode: int('.'), - }, pime.NewResponse(8, true)) - resp := ime.onKeyDown(&pime.Request{ - SeqNum: 9, - KeyCode: int('.'), - CharCode: int('.'), - }, pime.NewResponse(9, true)) - - if resp.ReturnValue != 1 { - t.Fatalf("expected punctuation to be handled while composing, got %d", resp.ReturnValue) - } - if resp.CommitString != "你." { - t.Fatalf("expected punctuation commit 你., got %q", resp.CommitString) - } -} - -func TestOnKeyDownUnhandledKeyReturnsZero(t *testing.T) { - ime := newTestIME() - - resp := ime.filterKeyDown(&pime.Request{ - SeqNum: 9, - KeyCode: 0x70, - CharCode: 0, - }, pime.NewResponse(9, true)) - - if resp.ReturnValue != 0 { - t.Fatalf("expected unrelated key to be ignored, got %d", resp.ReturnValue) - } -} - -func TestOnKeyDownAsciiModePassesThroughWhenIdle(t *testing.T) { - ime := newTestIME() - ime.backend.SetOption("ascii_mode", true) - - resp := ime.filterKeyDown(&pime.Request{ - SeqNum: 10, - KeyCode: int('A'), - CharCode: int('a'), - }, pime.NewResponse(10, true)) - - if resp.ReturnValue != 0 { - t.Fatalf("expected ascii mode to pass through idle typing, got %d", resp.ReturnValue) - } -} - -func TestControlKeyPassesThroughWhenIdle(t *testing.T) { - ime := newTestIME() - - resp := ime.filterKeyDown(&pime.Request{ - SeqNum: 10, - KeyCode: vkControl, - }, pime.NewResponse(10, true)) - - if resp.ReturnValue != 0 { - t.Fatalf("expected bare ctrl to pass through, got %d", resp.ReturnValue) - } -} - -// Regression: if filterKeyDown does not handle a bare Ctrl key, onKeyDown must return -// unhandled as well; otherwise the host still thinks the IME consumed the modifier. -func TestOnKeyDownBareControlUnhandledWhenFilterDoesNotHandle(t *testing.T) { - ime := newTestIME() - const seq = 20 - filterResp := ime.filterKeyDown(&pime.Request{ - SeqNum: seq, - KeyCode: vkControl, - }, pime.NewResponse(seq, true)) - if filterResp.ReturnValue != 0 { - t.Fatalf("expected filterKeyDown bare Ctrl unhandled, got %d", filterResp.ReturnValue) - } - onResp := ime.onKeyDown(&pime.Request{ - SeqNum: seq + 1, - KeyCode: vkControl, - }, pime.NewResponse(seq+1, true)) - if onResp.ReturnValue != 0 { - t.Fatalf("expected onKeyDown bare Ctrl unhandled when filter did not handle, got %d", onResp.ReturnValue) - } -} - -func TestOnKeyDownControlShortcutUnhandledWhenFilterDoesNotHandle(t *testing.T) { - ime := newTestIME() - const seq = 22 - filterResp := ime.filterKeyDown(&pime.Request{ - SeqNum: seq, - KeyCode: int('A'), - CharCode: 1, - }, pime.NewResponse(seq, true)) - if filterResp.ReturnValue != 0 { - t.Fatalf("expected filterKeyDown ctrl+a unhandled, got %d", filterResp.ReturnValue) - } - onResp := ime.onKeyDown(&pime.Request{ - SeqNum: seq + 1, - KeyCode: int('A'), - CharCode: 1, - }, pime.NewResponse(seq+1, true)) - if onResp.ReturnValue != 0 { - t.Fatalf("expected onKeyDown ctrl+a unhandled when filter did not handle, got %d", onResp.ReturnValue) - } -} - -// Regression: same contract as TestOnKeyDownBareControlUnhandledWhenFilterDoesNotHandle for key-up / Alt. -func TestOnKeyUpBareAltUnhandledWhenFilterDoesNotHandle(t *testing.T) { - ime := newTestIME() - const seq = 21 - filterResp := ime.filterKeyUp(&pime.Request{ - SeqNum: seq, - KeyCode: vkMenu, - }, pime.NewResponse(seq, true)) - if filterResp.ReturnValue != 0 { - t.Fatalf("expected filterKeyUp bare Alt unhandled, got %d", filterResp.ReturnValue) - } - onResp := ime.onKeyUp(&pime.Request{ - SeqNum: seq + 1, - KeyCode: vkMenu, - }, pime.NewResponse(seq+1, true)) - if onResp.ReturnValue != 0 { - t.Fatalf("expected onKeyUp bare Alt unhandled when filter did not handle, got %d", onResp.ReturnValue) - } -} - -func TestOnCommandHandlesKnownAndMissingCommand(t *testing.T) { - ime := newTestIME() - backend := ime.backend.(*testBackend) - backend.composition = "ni" - backend.refreshCandidates() - - validResp := ime.onCommand(&pime.Request{ - SeqNum: 11, - ID: pime.FlexibleID{Int: ID_ASCII_MODE, IsInt: true}, - }, pime.NewResponse(11, true)) - if validResp.ReturnValue != 1 { - t.Fatalf("expected known command to be handled, got %d", validResp.ReturnValue) - } - if !ime.backend.GetOption("ascii_mode") { - t.Fatal("expected ascii mode toggled on") - } - if backend.composition != "ni" { - t.Fatalf("expected test composition preserved until backend handles key flow, got %q", backend.composition) - } - - missingResp := ime.onCommand(&pime.Request{ - SeqNum: 12, - }, pime.NewResponse(12, true)) - if missingResp.ReturnValue != 0 { - t.Fatalf("expected missing commandId to be ignored, got %d", missingResp.ReturnValue) - } -} - -func TestOnMenuReturnsSettingsMenu(t *testing.T) { - ime := newTestIME() - - resp := ime.onMenu(&pime.Request{ - SeqNum: 15, - ID: pime.FlexibleID{String: "settings"}, - }, pime.NewResponse(15, true)) - - items, ok := resp.ReturnData.([]map[string]interface{}) - if !ok || len(items) == 0 { - t.Fatalf("expected settings menu items, got %#v", resp.ReturnData) - } - if text, ok := items[0]["text"].(string); !ok || text == "" { - t.Fatalf("expected first menu item text, got %#v", items[0]) - } -} - -func TestHandleRequestCompositionTerminatedResetsState(t *testing.T) { - ime := newTestIME() - backend := ime.backend.(*testBackend) - backend.composition = "ni" - backend.refreshCandidates() - - resp := ime.HandleRequest(&pime.Request{ - SeqNum: 13, - Method: "onCompositionTerminated", - }) - - if !resp.Success { - t.Fatal("expected composition termination response to succeed") - } - if backend.composition != "" || backend.candidates != nil { - t.Fatal("expected state reset on composition termination") - } -} - -func TestHandleRequestOnDeactivateReturnsHandled(t *testing.T) { - ime := newTestIME() - backend := ime.backend.(*testBackend) - backend.composition = "ni" - backend.refreshCandidates() - - resp := ime.HandleRequest(&pime.Request{ - SeqNum: 14, - Method: "onDeactivate", - }) - - if resp.ReturnValue != 1 { - t.Fatalf("expected onDeactivate to return 1, got %d", resp.ReturnValue) - } - if backend.composition != "" || backend.candidates != nil { - t.Fatal("expected deactivate to clear composition state") - } -} diff --git a/go-backend/pime/protocol.go b/go-backend/pime/protocol.go deleted file mode 100644 index f2709e7af..000000000 --- a/go-backend/pime/protocol.go +++ /dev/null @@ -1,185 +0,0 @@ -// PIME 通信协议定义 -package pime - -import ( - "encoding/json" - "fmt" -) - -// 消息类型 -const ( - MsgPIME = "PIME_MSG" -) - -type FlexibleID struct { - String string - Int int - IsInt bool -} - -type KeyStates []int - -func (k *KeyStates) UnmarshalJSON(data []byte) error { - var bools []bool - if err := json.Unmarshal(data, &bools); err == nil { - states := make(KeyStates, len(bools)) - for i, v := range bools { - if v { - states[i] = 1 - } - } - *k = states - return nil - } - - var ints []int - if err := json.Unmarshal(data, &ints); err == nil { - states := make(KeyStates, len(ints)) - copy(states, ints) - *k = states - return nil - } - - return fmt.Errorf("invalid keyStates payload: %s", string(data)) -} - -func (k KeyStates) IsKeyDown(code int) bool { - return code >= 0 && code < len(k) && (k[code]&(1<<7)) != 0 -} - -func (k KeyStates) IsKeyToggled(code int) bool { - return code >= 0 && code < len(k) && (k[code]&1) != 0 -} - -func (id FlexibleID) StringValue() string { - if id.IsInt { - return "" - } - return id.String -} - -func (id FlexibleID) IntValue() int { - if id.IsInt { - return id.Int - } - return 0 -} - -// Request PIME请求结构 -type Request struct { - Method string `json:"method"` - SeqNum int `json:"seqNum"` - ID FlexibleID `json:"-"` - IsWindows8Above bool `json:"isWindows8Above,omitempty"` - IsMetroApp bool `json:"isMetroApp,omitempty"` - IsUiLess bool `json:"isUiLess,omitempty"` - IsConsole bool `json:"isConsole,omitempty"` - IsKeyboardOpen bool `json:"isKeyboardOpen,omitempty"` - Opened bool `json:"opened,omitempty"` - Forced bool `json:"forced,omitempty"` - CommandType int `json:"type,omitempty"` - CharCode int `json:"charCode,omitempty"` - KeyCode int `json:"keyCode,omitempty"` - RepeatCount int `json:"repeatCount,omitempty"` - ScanCode int `json:"scanCode,omitempty"` - IsExtended bool `json:"isExtended,omitempty"` - KeyStates KeyStates `json:"keyStates,omitempty"` - CompositionString string `json:"compositionString,omitempty"` - CandidateList []string `json:"candidateList,omitempty"` - ShowCandidates bool `json:"showCandidates,omitempty"` - CursorPos int `json:"cursorPos,omitempty"` - SelStart int `json:"selStart,omitempty"` - SelEnd int `json:"selEnd,omitempty"` - // 扩展字段 - Data map[string]interface{} `json:"data,omitempty"` -} - -func (r *Request) UnmarshalJSON(data []byte) error { - type Alias Request - aux := &struct { - ID json.RawMessage `json:"id,omitempty"` - *Alias - }{ - Alias: (*Alias)(r), - } - if err := json.Unmarshal(data, aux); err != nil { - return err - } - if len(aux.ID) == 0 { - return nil - } - - var s string - if err := json.Unmarshal(aux.ID, &s); err == nil { - r.ID = FlexibleID{String: s} - return nil - } - - var n int - if err := json.Unmarshal(aux.ID, &n); err == nil { - r.ID = FlexibleID{Int: n, IsInt: true} - return nil - } - - return fmt.Errorf("invalid id payload: %s", string(aux.ID)) -} - -// ButtonInfo 按钮信息 -type ButtonInfo struct { - ID string `json:"id"` - Icon string `json:"icon,omitempty"` - Text string `json:"text,omitempty"` - Tooltip string `json:"tooltip,omitempty"` - CommandID int `json:"commandId,omitempty"` - Type string `json:"type,omitempty"` // "button", "toggle", "menu" - Enable bool `json:"enable,omitempty"` - Toggled bool `json:"toggled,omitempty"` -} - -// Response PIME响应结构 -type Response struct { - SeqNum int `json:"seqNum"` - Success bool `json:"success"` - ReturnValue int `json:"returnValue,omitempty"` - ReturnData interface{} `json:"-"` - CompositionString string `json:"compositionString"` - CommitString string `json:"commitString,omitempty"` - CandidateList []string `json:"candidateList"` - ShowCandidates bool `json:"showCandidates"` - CursorPos int `json:"cursorPos"` - CompositionCursor int `json:"compositionCursor"` - CandidateCursor int `json:"candidateCursor"` - SelStart int `json:"selStart"` - SelEnd int `json:"selEnd"` - SetSelKeys string `json:"setSelKeys,omitempty"` - Message string `json:"message,omitempty"` - CustomizeUI map[string]interface{} `json:"customizeUI,omitempty"` - // 按钮相关 - AddButton []ButtonInfo `json:"addButton,omitempty"` - RemoveButton []string `json:"removeButton,omitempty"` - ChangeButton []ButtonInfo `json:"changeButton,omitempty"` -} - -// ParseRequest 解析请求消息 -func ParseRequest(data []byte) (*Request, error) { - var req Request - if err := json.Unmarshal(data, &req); err != nil { - return nil, fmt.Errorf("unmarshal request failed: %w", err) - } - return &req, nil -} - -// ToJSON 转换为 JSON 字节 -func (r *Response) ToJSON() ([]byte, error) { - return json.Marshal(r) -} - -// NewResponse 创建新响应 -func NewResponse(seqNum int, success bool) *Response { - return &Response{ - SeqNum: seqNum, - Success: success, - CandidateList: []string{}, - CompositionString: "", - } -} diff --git a/go-backend/pime/protocol_test.go b/go-backend/pime/protocol_test.go deleted file mode 100644 index 448d8a3ee..000000000 --- a/go-backend/pime/protocol_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package pime - -import ( - "encoding/json" - "testing" -) - -func TestResponseJSONIncludesClearedCompositionState(t *testing.T) { - resp := NewResponse(1, true) - resp.ReturnValue = 1 - - payload, err := resp.ToJSON() - if err != nil { - t.Fatalf("ToJSON failed: %v", err) - } - - var decoded map[string]interface{} - if err := json.Unmarshal(payload, &decoded); err != nil { - t.Fatalf("unmarshal response failed: %v", err) - } - - if value, ok := decoded["compositionString"]; !ok || value != "" { - t.Fatalf("expected empty compositionString to be serialized, got %#v", decoded["compositionString"]) - } - if value, ok := decoded["showCandidates"]; !ok || value.(bool) { - t.Fatalf("expected showCandidates=false to be serialized, got %#v", decoded["showCandidates"]) - } - if value, ok := decoded["candidateList"]; !ok { - t.Fatalf("expected candidateList to be serialized, got %#v", decoded) - } else if list, ok := value.([]interface{}); !ok || len(list) != 0 { - t.Fatalf("expected empty candidateList, got %#v", value) - } -} - -func TestParseRequestAcceptsNumericKeyStates(t *testing.T) { - req, err := ParseRequest([]byte(`{ - "method": "onKeyDown", - "seqNum": 1, - "keyStates": [0, 1, 0, 2] - }`)) - if err != nil { - t.Fatalf("ParseRequest returned error: %v", err) - } - - want := []int{0, 1, 0, 2} - if len(req.KeyStates) != len(want) { - t.Fatalf("expected %d key states, got %d", len(want), len(req.KeyStates)) - } - for i, expected := range want { - if req.KeyStates[i] != expected { - t.Fatalf("expected keyStates[%d]=%d, got %d", i, expected, req.KeyStates[i]) - } - } -} - -func TestParseRequestAcceptsBooleanKeyStates(t *testing.T) { - req, err := ParseRequest([]byte(`{ - "method": "onKeyDown", - "seqNum": 1, - "keyStates": [true, false, true] - }`)) - if err != nil { - t.Fatalf("ParseRequest returned error: %v", err) - } - - want := []int{1, 0, 1} - if len(req.KeyStates) != len(want) { - t.Fatalf("expected %d key states, got %d", len(want), len(req.KeyStates)) - } - for i, expected := range want { - if req.KeyStates[i] != expected { - t.Fatalf("expected keyStates[%d]=%d, got %d", i, expected, req.KeyStates[i]) - } - } -} - -func TestParseRequestAcceptsStringAndNumericID(t *testing.T) { - stringReq, err := ParseRequest([]byte(`{"method":"init","seqNum":1,"id":"{guid}"}`)) - if err != nil { - t.Fatalf("ParseRequest returned error for string id: %v", err) - } - if got := stringReq.ID.StringValue(); got != "{guid}" { - t.Fatalf("expected string id {guid}, got %q", got) - } - - numericReq, err := ParseRequest([]byte(`{"method":"onCommand","seqNum":2,"id":3}`)) - if err != nil { - t.Fatalf("ParseRequest returned error for numeric id: %v", err) - } - if got := numericReq.ID.IntValue(); got != 3 { - t.Fatalf("expected numeric id 3, got %d", got) - } -} diff --git a/go-backend/pime/server.go b/go-backend/pime/server.go deleted file mode 100644 index b84b3e150..000000000 --- a/go-backend/pime/server.go +++ /dev/null @@ -1,183 +0,0 @@ -// PIME 服务器实现 -package pime - -import ( - "bufio" - "encoding/json" - "fmt" - "io" - "os" - "strings" - "sync" -) - -// Handler 请求处理函数类型 -type Handler func(clientID string, req *Request) *Response - -// Server PIME 服务器 -type Server struct { - mu sync.RWMutex - handlers map[string]Handler - clients map[string]*Client - reader *bufio.Reader - writer io.Writer - running bool -} - -// Client 客户端连接 -type Client struct { - ID string - GUID string - IsWindows8Above bool - IsMetroApp bool - IsUiLess bool - IsConsole bool - Service TextService -} - -// TextService 文本服务接口 -type TextService interface { - Init(req *Request) bool - HandleRequest(req *Request) *Response - Close() -} - -// NewServer 创建新服务器 -func NewServer() *Server { - return &Server{ - handlers: make(map[string]Handler), - clients: make(map[string]*Client), - reader: bufio.NewReader(os.Stdin), - writer: os.Stdout, - } -} - -// SetIO 设置自定义输入输出(用于测试) -func (s *Server) SetIO(reader io.Reader, writer io.Writer) { - s.reader = bufio.NewReader(reader) - s.writer = writer -} - -// RegisterHandler 注册方法处理器 -func (s *Server) RegisterHandler(method string, handler Handler) { - s.mu.Lock() - defer s.mu.Unlock() - s.handlers[method] = handler -} - -// Run 运行服务器 -func (s *Server) Run() error { - s.running = true - - for s.running { - line, err := s.reader.ReadString('\n') - if err != nil { - if err == io.EOF { - return nil - } - return fmt.Errorf("read error: %w", err) - } - - line = strings.TrimSpace(line) - if line == "" { - continue - } - - if err := s.handleMessage(line); err != nil { - fmt.Fprintf(os.Stderr, "handle message error: %v\n", err) - } - } - - return nil -} - -// Stop 停止服务器 -func (s *Server) Stop() { - s.running = false -} - -// handleMessage 处理消息 -func (s *Server) handleMessage(line string) error { - // 解析消息格式: "|" - parts := strings.SplitN(line, "|", 2) - if len(parts) != 2 { - return fmt.Errorf("invalid message format") - } - - clientID := parts[0] - jsonData := parts[1] - - // 解析 JSON 请求 - var req Request - if err := json.Unmarshal([]byte(jsonData), &req); err != nil { - return fmt.Errorf("unmarshal request failed: %w", err) - } - - // 处理请求 - var resp *Response - - // 特殊处理 init 方法 - if req.Method == "" && req.ID.StringValue() != "" { - // 初始化请求 - resp = s.handleInit(clientID, &req) - } else if handler, ok := s.handlers[req.Method]; ok { - resp = handler(clientID, &req) - } else { - // 默认处理 - resp = NewResponse(req.SeqNum, true) - } - - // 发送响应 - return s.sendResponse(clientID, resp) -} - -// handleInit 处理初始化 -func (s *Server) handleInit(clientID string, req *Request) *Response { - s.mu.Lock() - defer s.mu.Unlock() - - // 创建客户端 - client := &Client{ - ID: clientID, - IsWindows8Above: req.IsWindows8Above, - IsMetroApp: req.IsMetroApp, - IsUiLess: req.IsUiLess, - IsConsole: req.IsConsole, - } - s.clients[clientID] = client - - resp := NewResponse(req.SeqNum, true) - return resp -} - -// sendResponse 发送响应 -func (s *Server) sendResponse(clientID string, resp *Response) error { - // 格式: "PIME_MSG||" - jsonData, err := resp.ToJSON() - if err != nil { - return err - } - - line := fmt.Sprintf("PIME_MSG|%s|%s\n", clientID, string(jsonData)) - _, err = s.writer.Write([]byte(line)) - return err -} - -// GetClient 获取客户端 -func (s *Server) GetClient(clientID string) *Client { - s.mu.RLock() - defer s.mu.RUnlock() - return s.clients[clientID] -} - -// RemoveClient 移除客户端 -func (s *Server) RemoveClient(clientID string) { - s.mu.Lock() - defer s.mu.Unlock() - if client, ok := s.clients[clientID]; ok { - if client.Service != nil { - client.Service.Close() - } - delete(s.clients, clientID) - } -} diff --git a/go-backend/pime/service.go b/go-backend/pime/service.go deleted file mode 100644 index 9b160dc17..000000000 --- a/go-backend/pime/service.go +++ /dev/null @@ -1,107 +0,0 @@ -// PIME 文本服务接口和基础实现 -package pime - -// TextServiceBase 文本服务基础实现 -type TextServiceBase struct { - Client *Client - Composition string - CandidateList []string - CursorPos int -} - -// NewTextServiceBase 创建基础文本服务 -func NewTextServiceBase(client *Client) *TextServiceBase { - return &TextServiceBase{ - Client: client, - Composition: "", - CandidateList: make([]string, 0), - CursorPos: 0, - } -} - -// Init 初始化服务 -func (s *TextServiceBase) Init(req *Request) bool { - return true -} - -// HandleRequest 处理请求 -func (s *TextServiceBase) HandleRequest(req *Request) *Response { - resp := NewResponse(req.SeqNum, true) - - switch req.Method { - case "onActivate": - s.onActivate(req, resp) - case "onDeactivate": - s.onDeactivate(req, resp) - case "filterKeyDown": - s.filterKeyDown(req, resp) - case "filterKeyUp": - s.filterKeyUp(req, resp) - case "onCompositionTerminated": - s.onCompositionTerminated(req, resp) - case "onCommand": - s.onCommand(req, resp) - case "onMenu": - s.onMenu(req, resp) - } - - return resp -} - -// Close 关闭服务 -func (s *TextServiceBase) Close() { - // 清理资源 -} - -// 事件处理方法(可被子类覆盖) - -func (s *TextServiceBase) onActivate(req *Request, resp *Response) { - // 输入法激活时的处理 -} - -func (s *TextServiceBase) onDeactivate(req *Request, resp *Response) { - // 输入法失活时的处理 -} - -func (s *TextServiceBase) filterKeyDown(req *Request, resp *Response) { - // 按键按下处理 - // 返回 returnValue: 0=不处理, 1=已处理 - resp.ReturnValue = 0 -} - -func (s *TextServiceBase) filterKeyUp(req *Request, resp *Response) { - // 按键释放处理 - resp.ReturnValue = 0 -} - -func (s *TextServiceBase) onCompositionTerminated(req *Request, resp *Response) { - // 组合终止处理 -} - -func (s *TextServiceBase) onCommand(req *Request, resp *Response) { - // 命令处理 -} - -func (s *TextServiceBase) onMenu(req *Request, resp *Response) { - // 菜单请求,默认无菜单 - resp.ReturnData = nil -} - -// Helper 方法 - -// UpdateComposition 更新组合字符串 -func (s *TextServiceBase) UpdateComposition(resp *Response, composition string, cursorPos int) { - resp.CompositionString = composition - resp.CursorPos = cursorPos -} - -// SetCandidates 设置候选词列表 -func (s *TextServiceBase) SetCandidates(resp *Response, candidates []string, show bool) { - resp.CandidateList = candidates - resp.ShowCandidates = show -} - -// CommitString 提交字符串 -func (s *TextServiceBase) CommitString(resp *Response, text string) { - resp.CommitString = text -} diff --git a/go-backend/pime/service_manager.go b/go-backend/pime/service_manager.go deleted file mode 100644 index 26d0334a3..000000000 --- a/go-backend/pime/service_manager.go +++ /dev/null @@ -1,166 +0,0 @@ -// 服务管理器 -package pime - -import ( - "bufio" - "encoding/json" - "fmt" - "os" - "strings" - "sync" -) - -// ServiceFactory 服务工厂函数类型 -type ServiceFactory func(clientID string) TextService - -// ServiceManager 服务管理器 -type ServiceManager struct { - mu sync.RWMutex - factories map[string]ServiceFactory - services map[string]TextService - reader *bufio.Reader - running bool -} - -// NewServiceManager 创建服务管理器 -func NewServiceManager() *ServiceManager { - return &ServiceManager{ - factories: make(map[string]ServiceFactory), - services: make(map[string]TextService), - reader: bufio.NewReader(os.Stdin), - } -} - -// Register 注册服务工厂 -func (m *ServiceManager) Register(name string, factory ServiceFactory) { - m.mu.Lock() - defer m.mu.Unlock() - m.factories[name] = factory -} - -// Run 运行服务管理器 -func (m *ServiceManager) Run() error { - m.running = true - - for m.running { - line, err := m.reader.ReadString('\n') - if err != nil { - return err - } - - line = strings.TrimSpace(line) - if line == "" { - continue - } - - if err := m.handleMessage(line); err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - } - } - - return nil -} - -// handleMessage 处理消息 -func (m *ServiceManager) handleMessage(line string) error { - // 解析格式: "|" - parts := strings.SplitN(line, "|", 2) - if len(parts) != 2 { - return fmt.Errorf("invalid message format") - } - - clientID := parts[0] - jsonStr := parts[1] - - // 解析 JSON - var msg map[string]interface{} - if err := json.Unmarshal([]byte(jsonStr), &msg); err != nil { - return err - } - - // 处理消息 - response := m.processMessage(clientID, msg) - - // 发送响应 - return m.sendResponse(clientID, response) -} - -// processMessage 处理消息 -func (m *ServiceManager) processMessage(clientID string, msg map[string]interface{}) map[string]interface{} { - response := make(map[string]interface{}) - - // 获取序列号 - if seqNum, ok := msg["seqNum"].(float64); ok { - response["seqNum"] = int(seqNum) - } - - // 获取方法 - method, _ := msg["method"].(string) - - switch method { - case "init": - // 初始化 - response["success"] = m.initClient(clientID, msg) - - case "close": - // 关闭客户端 - m.closeClient(clientID) - response["success"] = true - - default: - // 其他方法,转发给服务处理 - if service := m.getService(clientID); service != nil { - // 这里需要实现具体的请求处理 - response["success"] = true - } else { - response["success"] = false - } - } - - return response -} - -// initClient 初始化客户端 -func (m *ServiceManager) initClient(clientID string, msg map[string]interface{}) bool { - m.mu.Lock() - defer m.mu.Unlock() - - // 创建默认服务(使用第一个注册的工厂) - for _, factory := range m.factories { - service := factory(clientID) - m.services[clientID] = service - return true - } - - return false -} - -// closeClient 关闭客户端 -func (m *ServiceManager) closeClient(clientID string) { - m.mu.Lock() - defer m.mu.Unlock() - - if service, ok := m.services[clientID]; ok { - service.Close() - delete(m.services, clientID) - } -} - -// getService 获取服务 -func (m *ServiceManager) getService(clientID string) TextService { - m.mu.RLock() - defer m.mu.RUnlock() - return m.services[clientID] -} - -// sendResponse 发送响应 -func (m *ServiceManager) sendResponse(clientID string, response map[string]interface{}) error { - data, err := json.Marshal(response) - if err != nil { - return err - } - - line := fmt.Sprintf("PIME_MSG|%s|%s\n", clientID, string(data)) - _, err = os.Stdout.WriteString(line) - return err -} diff --git a/go-backend/pime/tray.go b/go-backend/pime/tray.go deleted file mode 100644 index a0c328afc..000000000 --- a/go-backend/pime/tray.go +++ /dev/null @@ -1,77 +0,0 @@ -package pime - -import ( - "os" - "path/filepath" -) - -func sharedRimeIconPath(iconName string) string { - exePath, err := os.Executable() - if err != nil { - return "" - } - iconPath := filepath.Join(filepath.Dir(exePath), "input_methods", "rime", "icons", iconName) - if _, err := os.Stat(iconPath); err != nil { - return "" - } - return iconPath -} - -func modeIconName(asciiMode bool) string { - if asciiMode { - return "eng_half_capsoff.ico" - } - return "chi_half_capsoff.ico" -} - -func langIconName(asciiMode bool) string { - if asciiMode { - return "eng.ico" - } - return "chi.ico" -} - -func AddLangButtons(resp *Response, client *Client, asciiMode bool, modeCommandID int, langCommandID int) { - if client != nil && client.IsWindows8Above { - if iconPath := sharedRimeIconPath(modeIconName(asciiMode)); iconPath != "" { - resp.AddButton = append(resp.AddButton, ButtonInfo{ - ID: "windows-mode-icon", - Icon: iconPath, - Tooltip: "中西文切换", - CommandID: modeCommandID, - }) - } - } - if iconPath := sharedRimeIconPath(langIconName(asciiMode)); iconPath != "" { - resp.AddButton = append(resp.AddButton, ButtonInfo{ - ID: "switch-lang", - Icon: iconPath, - Tooltip: "中西文切换", - CommandID: langCommandID, - }) - } -} - -func ChangeLangButtons(resp *Response, client *Client, asciiMode bool) { - if client != nil && client.IsWindows8Above { - if iconPath := sharedRimeIconPath(modeIconName(asciiMode)); iconPath != "" { - resp.ChangeButton = append(resp.ChangeButton, ButtonInfo{ - ID: "windows-mode-icon", - Icon: iconPath, - }) - } - } - if iconPath := sharedRimeIconPath(langIconName(asciiMode)); iconPath != "" { - resp.ChangeButton = append(resp.ChangeButton, ButtonInfo{ - ID: "switch-lang", - Icon: iconPath, - }) - } -} - -func RemoveLangButtons(resp *Response, client *Client) { - if client != nil && client.IsWindows8Above { - resp.RemoveButton = append(resp.RemoveButton, "windows-mode-icon") - } - resp.RemoveButton = append(resp.RemoveButton, "switch-lang") -} diff --git a/go-backend/server.go b/go-backend/server.go deleted file mode 100644 index c5495c887..000000000 --- a/go-backend/server.go +++ /dev/null @@ -1,473 +0,0 @@ -// PIME Go 后端主入口 -// 参考 python/server.py 实现 -package main - -import ( - "bufio" - "encoding/json" - "fmt" - "log" - "os" - "path/filepath" - "strings" - "sync" - - "github.com/EasyIME/pime-go/pime" - - // 导入输入法包 - "github.com/EasyIME/pime-go/input_methods/fcitx5" - "github.com/EasyIME/pime-go/input_methods/meow" - "github.com/EasyIME/pime-go/input_methods/rime" - simplepinyin "github.com/EasyIME/pime-go/input_methods/simple_pinyin" -) - -// Client 客户端连接 -type Client struct { - ID string - GUID string - IsWindows8Above bool - IsMetroApp bool - IsUiLess bool - IsConsole bool - Service pime.TextService -} - -// ServiceFactory 服务工厂函数 -type ServiceFactory func(client *pime.Client, guid string) pime.TextService - -// Server PIME 服务器 -type Server struct { - mu sync.RWMutex - clients map[string]*Client - factories map[string]ServiceFactory // guid -> factory - reader *bufio.Reader - running bool -} - -func stringifyData(data map[string]interface{}) string { - if len(data) == 0 { - return "" - } - raw, err := json.Marshal(data) - if err != nil { - return fmt.Sprintf("", err) - } - return string(raw) -} - -func logRequestSummary(clientID string, req *pime.Request) { - log.Printf( - "收到请求 client=%s method=%s seq=%d id=%q commandId=%d keyCode=%d charCode=%d repeat=%d scan=%d composing=%q candidates=%d showCandidates=%t cursor=%d data=%s", - clientID, - req.Method, - req.SeqNum, - req.ID.StringValue(), - req.ID.IntValue(), - req.KeyCode, - req.CharCode, - req.RepeatCount, - req.ScanCode, - req.CompositionString, - len(req.CandidateList), - req.ShowCandidates, - req.CursorPos, - stringifyData(req.Data), - ) -} - -func logResponseSummary(clientID string, resp map[string]interface{}) { - raw, err := json.Marshal(resp) - if err != nil { - log.Printf("发送响应 client=%s marshal_error=%v", clientID, err) - return - } - log.Printf("发送响应 client=%s payload=%s", clientID, string(raw)) -} - -// NewServer 创建服务器 -func NewServer() *Server { - return &Server{ - clients: make(map[string]*Client), - factories: make(map[string]ServiceFactory), - reader: bufio.NewReader(os.Stdin), - } -} - -// RegisterService 注册输入法服务工厂 -func (s *Server) RegisterService(guid string, factory ServiceFactory) { - s.mu.Lock() - defer s.mu.Unlock() - guid = strings.ToLower(guid) - s.factories[guid] = factory - log.Printf("注册输入法服务: %s", guid) -} - -// Run 运行服务器 -func (s *Server) Run() error { - s.running = true - log.Println("PIME Go 后端服务器已启动") - - for s.running { - line, err := s.reader.ReadString('\n') - if err != nil { - if err.Error() == "EOF" { - log.Println("收到 EOF,服务器停止") - return nil - } - return fmt.Errorf("读取错误: %w", err) - } - - line = strings.TrimSpace(line) - if line == "" { - continue - } - - if err := s.handleMessage(line); err != nil { - log.Printf("处理消息错误: %v", err) - // 发送错误响应,防止客户端阻塞 - parts := strings.SplitN(line, "|", 2) - if len(parts) == 2 { - s.sendResponse(parts[0], map[string]interface{}{ - "success": false, - "error": err.Error(), - }) - } - } - } - - return nil -} - -// handleMessage 处理消息 -// 格式: "|" -func (s *Server) handleMessage(line string) error { - parts := strings.SplitN(line, "|", 2) - if len(parts) != 2 { - return fmt.Errorf("无效的消息格式") - } - - clientID := parts[0] - jsonData := parts[1] - - var req pime.Request - if err := json.Unmarshal([]byte(jsonData), &req); err != nil { - return fmt.Errorf("解析 JSON 失败: %w", err) - } - - logRequestSummary(clientID, &req) - - // 处理请求 - resp := s.handleRequest(clientID, &req) - - // 发送响应 - return s.sendResponse(clientID, resp) -} - -// handleRequest 处理请求 -func (s *Server) handleRequest(clientID string, req *pime.Request) map[string]interface{} { - s.mu.Lock() - defer s.mu.Unlock() - - switch req.Method { - case "init": - // PIME 在 init 时通过顶层 id 传递语言配置 GUID。 - // 为了兼容已有调用,也接受 data.guid。 - guid := req.ID.StringValue() - if guid == "" && req.Data != nil { - guid, _ = req.Data["guid"].(string) - } - guid = strings.ToLower(guid) - if guid == "" { - log.Printf("初始化失败 client=%s seq=%d 原因=缺少guid id=%q data=%s", clientID, req.SeqNum, req.ID.StringValue(), stringifyData(req.Data)) - return map[string]interface{}{ - "seqNum": req.SeqNum, - "success": false, - "error": "缺少 guid", - } - } - - // 创建客户端 - client := &Client{ - ID: clientID, - GUID: guid, - IsWindows8Above: req.IsWindows8Above, - IsMetroApp: req.IsMetroApp, - IsUiLess: req.IsUiLess, - IsConsole: req.IsConsole, - } - - // 获取输入法服务工厂 - factory, ok := s.factories[guid] - if !ok { - log.Printf("初始化失败 client=%s seq=%d 原因=未知输入法 guid=%s", clientID, req.SeqNum, guid) - return map[string]interface{}{ - "seqNum": req.SeqNum, - "success": false, - "error": fmt.Sprintf("未知的输入法: %s", guid), - } - } - - // 创建输入法服务 - pimeClient := &pime.Client{ - ID: clientID, - GUID: guid, - IsWindows8Above: req.IsWindows8Above, - IsMetroApp: req.IsMetroApp, - IsUiLess: req.IsUiLess, - IsConsole: req.IsConsole, - } - client.Service = factory(pimeClient, guid) - s.clients[clientID] = client - - // 初始化服务 - if !client.Service.Init(req) { - delete(s.clients, clientID) - log.Printf("初始化失败 client=%s seq=%d guid=%s 原因=Service.Init返回false", clientID, req.SeqNum, guid) - return map[string]interface{}{ - "seqNum": req.SeqNum, - "success": false, - "error": "初始化失败", - } - } - - log.Printf("初始化成功 client=%s seq=%d guid=%s windows8=%t metro=%t uiless=%t console=%t", clientID, req.SeqNum, guid, req.IsWindows8Above, req.IsMetroApp, req.IsUiLess, req.IsConsole) - - return map[string]interface{}{ - "seqNum": req.SeqNum, - "success": true, - } - - case "close": - if client, ok := s.clients[clientID]; ok { - client.Service.Close() - delete(s.clients, clientID) - log.Printf("客户端关闭 client=%s guid=%s", clientID, client.GUID) - } else { - log.Printf("客户端关闭 client=%s 但未找到已初始化会话", clientID) - } - return map[string]interface{}{ - "seqNum": req.SeqNum, - "success": true, - } - - case "onActivate", "onDeactivate", "filterKeyDown", "onKeyDown", - "filterKeyUp", "onKeyUp", "onCommand", "onMenu", "onCompositionTerminated", - "onPreservedKey", "onLangProfileActivated": - // 转发到输入法服务 - client, ok := s.clients[clientID] - if !ok { - log.Printf("请求失败 client=%s seq=%d method=%s 原因=客户端未初始化", clientID, req.SeqNum, req.Method) - return map[string]interface{}{ - "seqNum": req.SeqNum, - "success": false, - "error": "客户端未初始化", - } - } - - log.Printf("转发请求 client=%s seq=%d method=%s guid=%s", clientID, req.SeqNum, req.Method, client.GUID) - resp := client.Service.HandleRequest(req) - return s.convertResponse(resp) - - default: - log.Printf("请求失败 client=%s seq=%d method=%s 原因=未知方法", clientID, req.SeqNum, req.Method) - return map[string]interface{}{ - "seqNum": req.SeqNum, - "success": false, - "error": fmt.Sprintf("未知的方法: %s", req.Method), - } - } -} - -// sendResponse 发送响应 -func (s *Server) sendResponse(clientID string, resp map[string]interface{}) error { - logResponseSummary(clientID, resp) - data, err := json.Marshal(resp) - if err != nil { - return fmt.Errorf("序列化响应失败: %w", err) - } - - fmt.Printf("%s|%s|%s\n", pime.MsgPIME, clientID, string(data)) - return nil -} - -// convertResponse 转换响应格式 -func (s *Server) convertResponse(resp *pime.Response) map[string]interface{} { - candidateList := resp.CandidateList - if candidateList == nil { - candidateList = []string{} - } - m := map[string]interface{}{ - "success": resp.Success, - "seqNum": resp.SeqNum, - "cursorPos": resp.CursorPos, - "compositionCursor": resp.CompositionCursor, - "candidateCursor": resp.CandidateCursor, - "showCandidates": resp.ShowCandidates, - "compositionString": resp.CompositionString, - "candidateList": candidateList, - "selStart": resp.SelStart, - "selEnd": resp.SelEnd, - } - if resp.ReturnData != nil { - m["return"] = resp.ReturnData - } else { - m["return"] = resp.ReturnValue - } - - if resp.CommitString != "" { - m["commitString"] = resp.CommitString - } - if resp.SetSelKeys != "" { - m["setSelKeys"] = resp.SetSelKeys - } - if len(resp.CustomizeUI) > 0 { - m["customizeUI"] = resp.CustomizeUI - } - if len(resp.AddButton) > 0 { - m["addButton"] = resp.AddButton - } - if len(resp.RemoveButton) > 0 { - m["removeButton"] = resp.RemoveButton - } - if len(resp.ChangeButton) > 0 { - m["changeButton"] = resp.ChangeButton - } - return m -} - -// loadInputMethods 加载所有输入法 -func loadInputMethods(server *Server) { - // 获取当前目录 - exePath, err := os.Executable() - if err != nil { - log.Fatal("获取可执行文件路径失败:", err) - } - exeDir := filepath.Dir(exePath) - - // 扫描 input_methods 目录 - inputMethodsDir := filepath.Join(exeDir, "input_methods") - entries, err := os.ReadDir(inputMethodsDir) - if err != nil { - log.Printf("读取 input_methods 目录失败: %v", err) - return - } - - for _, entry := range entries { - if !entry.IsDir() { - continue - } - - // 读取 ime.json - imePath := filepath.Join(inputMethodsDir, entry.Name(), "ime.json") - data, err := os.ReadFile(imePath) - if err != nil { - log.Printf("读取 %s 失败: %v", imePath, err) - continue - } - - var imeConfig map[string]interface{} - if err := json.Unmarshal(data, &imeConfig); err != nil { - log.Printf("解析 %s 失败: %v", imePath, err) - continue - } - - guid, _ := imeConfig["guid"].(string) - name, _ := imeConfig["name"].(string) - guid = strings.ToLower(guid) - if guid == "" { - log.Printf("%s 缺少 guid", entry.Name()) - continue - } - - log.Printf("加载输入法: %s (%s)", name, guid) - - // 根据输入法名称注册不同的服务实现 - switch entry.Name() { - case "meow": - // 喵喵输入法 - server.RegisterService(guid, func(client *pime.Client, g string) pime.TextService { - return meow.New(client) - }) - case "rime": - // RIME 输入法 - server.RegisterService(guid, func(client *pime.Client, g string) pime.TextService { - return rime.New(client) - }) - case "simple_pinyin": - // 拼音输入法 - server.RegisterService(guid, func(client *pime.Client, g string) pime.TextService { - return simplepinyin.New(client) - }) - case "fcitx5": - // Fcitx5 输入法 - server.RegisterService(guid, func(client *pime.Client, g string) pime.TextService { - return fcitx5.New(client) - }) - default: - // 默认使用拼音输入法 - server.RegisterService(guid, func(client *pime.Client, g string) pime.TextService { - return simplepinyin.New(client) - }) - } - } -} - -func openLogFile() (*os.File, error) { - candidates := []string{} - - if localAppData := os.Getenv("LOCALAPPDATA"); localAppData != "" { - candidates = append(candidates, filepath.Join(localAppData, "PIME", "Logs", "go_backend.log")) - } - if tempDir := os.TempDir(); tempDir != "" { - candidates = append(candidates, filepath.Join(tempDir, "PIME", "go_backend.log")) - } - candidates = append(candidates, "go_backend.log") - - var lastErr error - for _, logPath := range candidates { - logDir := filepath.Dir(logPath) - if logDir != "." && logDir != "" { - if err := os.MkdirAll(logDir, 0755); err != nil { - lastErr = err - continue - } - } - - logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) - if err == nil { - return logFile, nil - } - lastErr = err - } - - return nil, lastErr -} - -func main() { - // 日志优先写入用户可写目录,避免安装到 Program Files 后启动失败。 - logFile, err := openLogFile() - if err != nil { - log.SetOutput(os.Stderr) - log.SetFlags(log.LstdFlags | log.Lshortfile) - log.Printf("无法创建日志文件,改用标准错误输出: %v", err) - } else { - defer logFile.Close() - log.SetOutput(logFile) - log.SetFlags(log.LstdFlags | log.Lshortfile) - } - - log.Println("=" + strings.Repeat("=", 50)) - log.Println("PIME Go 后端启动") - log.Println("=" + strings.Repeat("=", 50)) - - // 创建服务器 - server := NewServer() - - // 加载所有输入法 - loadInputMethods(server) - - // 运行服务器 - if err := server.Run(); err != nil { - log.Fatal("服务器错误:", err) - } -} diff --git a/go-backend/server_integration_test.go b/go-backend/server_integration_test.go deleted file mode 100644 index 3119c416d..000000000 --- a/go-backend/server_integration_test.go +++ /dev/null @@ -1,530 +0,0 @@ -package main - -import ( - "encoding/json" - "io" - "os" - "strings" - "testing" - - fcitx5ime "github.com/EasyIME/pime-go/input_methods/fcitx5" - meowime "github.com/EasyIME/pime-go/input_methods/meow" - rimeime "github.com/EasyIME/pime-go/input_methods/rime" - simplepinyinime "github.com/EasyIME/pime-go/input_methods/simple_pinyin" - "github.com/EasyIME/pime-go/pime" -) - -const testMeowGUID = "{7A1C2E93-5B64-4F88-AE21-3D9C6B70F145}" -const testSimplePinyinGUID = "{5C8E1D74-2F9A-4B63-91DE-7A45C8F2B306}" -const testRimeGUID = "{3F6B5A12-8D44-4E71-9A2E-6B4F9C1D2A30}" -const testFcitx5GUID = "{D2E4A8B1-6C35-4F90-AB7D-18E2635C9F41}" - -func newTestServerWithMeow() *Server { - server := NewServer() - server.RegisterService(testMeowGUID, func(client *pime.Client, guid string) pime.TextService { - return meowime.New(client) - }) - return server -} - -func newTestServerWithSimplePinyin() *Server { - server := NewServer() - server.RegisterService(testSimplePinyinGUID, func(client *pime.Client, guid string) pime.TextService { - return simplepinyinime.New(client) - }) - return server -} - -func newTestServerWithRime() *Server { - server := NewServer() - server.RegisterService(testRimeGUID, func(client *pime.Client, guid string) pime.TextService { - return rimeime.New(client) - }) - return server -} - -func newTestServerWithFcitx5() *Server { - server := NewServer() - server.RegisterService(testFcitx5GUID, func(client *pime.Client, guid string) pime.TextService { - return fcitx5ime.New(client) - }) - return server -} - -func captureStdout(t *testing.T, fn func()) string { - t.Helper() - - oldStdout := os.Stdout - reader, writer, err := os.Pipe() - if err != nil { - t.Fatalf("create stdout pipe: %v", err) - } - - os.Stdout = writer - defer func() { - os.Stdout = oldStdout - }() - - fn() - - if err := writer.Close(); err != nil { - t.Fatalf("close stdout writer: %v", err) - } - - output, err := io.ReadAll(reader) - if err != nil { - t.Fatalf("read captured stdout: %v", err) - } - if err := reader.Close(); err != nil { - t.Fatalf("close stdout reader: %v", err) - } - - return strings.TrimSpace(string(output)) -} - -func sendProtocolMessage(t *testing.T, server *Server, clientID string, payload map[string]interface{}) (string, map[string]interface{}) { - t.Helper() - - data, err := json.Marshal(payload) - if err != nil { - t.Fatalf("marshal payload: %v", err) - } - - line := clientID + "|" + string(data) - output := captureStdout(t, func() { - if err := server.handleMessage(line); err != nil { - t.Fatalf("handleMessage failed: %v", err) - } - }) - - prefix := pime.MsgPIME + "|" + clientID + "|" - if !strings.HasPrefix(output, prefix) { - t.Fatalf("expected %q prefix, got %q", prefix, output) - } - - var response map[string]interface{} - if err := json.Unmarshal([]byte(strings.TrimPrefix(output, prefix)), &response); err != nil { - t.Fatalf("unmarshal response: %v", err) - } - - return output, response -} - -func TestServerHandleMessageInitUsesTopLevelID(t *testing.T) { - server := newTestServerWithMeow() - - _, response := sendProtocolMessage(t, server, "client-1", map[string]interface{}{ - "method": "init", - "seqNum": 1, - "id": testMeowGUID, - "isWindows8Above": true, - "isMetroApp": false, - "isUiLess": false, - "isConsole": false, - }) - - if response["success"] != true { - t.Fatalf("expected init success, got %#v", response) - } - if response["seqNum"] != float64(1) { - t.Fatalf("expected seqNum 1, got %#v", response["seqNum"]) - } - - client := server.clients["client-1"] - if client == nil { - t.Fatal("expected client to be registered after init") - } - if client.GUID != strings.ToLower(testMeowGUID) { - t.Fatalf("expected guid %q, got %q", strings.ToLower(testMeowGUID), client.GUID) - } -} - -func TestServerHandleMessageInitAcceptsLowercaseGUID(t *testing.T) { - server := newTestServerWithMeow() - - _, response := sendProtocolMessage(t, server, "client-lower", map[string]interface{}{ - "method": "init", - "seqNum": 1, - "id": strings.ToLower(testMeowGUID), - "isWindows8Above": true, - "isMetroApp": false, - "isUiLess": false, - "isConsole": false, - }) - - if response["success"] != true { - t.Fatalf("expected lowercase guid init success, got %#v", response) - } -} - -func TestServerHandleMessageMeowRequestResponseFlow(t *testing.T) { - server := newTestServerWithMeow() - - sendProtocolMessage(t, server, "client-2", map[string]interface{}{ - "method": "init", - "seqNum": 1, - "id": testMeowGUID, - "isWindows8Above": true, - "isMetroApp": false, - "isUiLess": false, - "isConsole": false, - }) - - _, filterResp := sendProtocolMessage(t, server, "client-2", map[string]interface{}{ - "method": "filterKeyDown", - "seqNum": 2, - "keyCode": 0x4D, - "charCode": 'm', - }) - if filterResp["return"] != float64(1) { - t.Fatalf("expected filterKeyDown to handle m, got %#v", filterResp) - } - - _, firstKeyResp := sendProtocolMessage(t, server, "client-2", map[string]interface{}{ - "method": "onKeyDown", - "seqNum": 3, - "keyCode": 0x4D, - "charCode": 'm', - }) - if firstKeyResp["compositionString"] != "喵" { - t.Fatalf("expected first m to build composition 喵, got %#v", firstKeyResp) - } - if firstKeyResp["return"] != float64(1) { - t.Fatalf("expected first m return 1, got %#v", firstKeyResp) - } - - _, secondKeyResp := sendProtocolMessage(t, server, "client-2", map[string]interface{}{ - "method": "onKeyDown", - "seqNum": 4, - "keyCode": 0x4D, - "charCode": 'm', - }) - if secondKeyResp["showCandidates"] != true { - t.Fatalf("expected second m to show candidates, got %#v", secondKeyResp) - } - candidateList, ok := secondKeyResp["candidateList"].([]interface{}) - if !ok { - t.Fatalf("expected candidate list array, got %#v", secondKeyResp["candidateList"]) - } - if len(candidateList) != 4 { - t.Fatalf("expected 4 candidates, got %d", len(candidateList)) - } - if candidateList[1] != "描" { - t.Fatalf("expected second candidate 描, got %#v", candidateList[1]) - } - - _, selectResp := sendProtocolMessage(t, server, "client-2", map[string]interface{}{ - "method": "onKeyDown", - "seqNum": 5, - "keyCode": 0x32, - }) - if selectResp["commitString"] != "描" { - t.Fatalf("expected number key to commit 描, got %#v", selectResp) - } - if selectResp["showCandidates"] != false { - t.Fatalf("expected candidate window to close, got %#v", selectResp) - } - if selectResp["return"] != float64(1) { - t.Fatalf("expected candidate selection return 1, got %#v", selectResp) - } -} - -func TestServerHandleMessageUninitializedClientReturnsProtocolError(t *testing.T) { - server := newTestServerWithMeow() - - _, response := sendProtocolMessage(t, server, "client-3", map[string]interface{}{ - "method": "onKeyDown", - "seqNum": 9, - "keyCode": 0x4D, - "charCode": 'm', - }) - - if response["success"] != false { - t.Fatalf("expected uninitialized client to fail, got %#v", response) - } - if response["seqNum"] != float64(9) { - t.Fatalf("expected seqNum 9, got %#v", response["seqNum"]) - } - if response["error"] != "客户端未初始化" { - t.Fatalf("expected protocol error for uninitialized client, got %#v", response["error"]) - } -} - -func TestServerHandleMessageCloseSucceeds(t *testing.T) { - server := newTestServerWithMeow() - - sendProtocolMessage(t, server, "client-close", map[string]interface{}{ - "method": "init", - "seqNum": 1, - "id": testMeowGUID, - "isWindows8Above": true, - "isMetroApp": false, - "isUiLess": false, - "isConsole": false, - }) - - _, response := sendProtocolMessage(t, server, "client-close", map[string]interface{}{ - "method": "close", - "seqNum": 2, - }) - - if response["success"] != true { - t.Fatalf("expected close success, got %#v", response) - } - if _, ok := server.clients["client-close"]; ok { - t.Fatal("expected client to be removed after close") - } -} - -func TestServerHandleMessageSimplePinyinRequestResponseFlow(t *testing.T) { - server := newTestServerWithSimplePinyin() - - sendProtocolMessage(t, server, "client-4", map[string]interface{}{ - "method": "init", - "seqNum": 1, - "id": testSimplePinyinGUID, - "isWindows8Above": true, - "isMetroApp": false, - "isUiLess": false, - "isConsole": false, - }) - - _, firstResp := sendProtocolMessage(t, server, "client-4", map[string]interface{}{ - "method": "filterKeyDown", - "seqNum": 2, - "keyCode": 0x4E, - "charCode": 'n', - }) - if firstResp["compositionString"] != "n" { - t.Fatalf("expected first key to build composition n, got %#v", firstResp) - } - if firstResp["return"] != float64(1) { - t.Fatalf("expected first key return 1, got %#v", firstResp) - } - - _, secondResp := sendProtocolMessage(t, server, "client-4", map[string]interface{}{ - "method": "filterKeyDown", - "seqNum": 3, - "keyCode": 0x49, - "charCode": 'i', - }) - if secondResp["compositionString"] != "ni" { - t.Fatalf("expected second key to build composition ni, got %#v", secondResp) - } - candidateList, ok := secondResp["candidateList"].([]interface{}) - if !ok { - t.Fatalf("expected candidate list array, got %#v", secondResp["candidateList"]) - } - if len(candidateList) != 3 { - t.Fatalf("expected fallback candidate count 3, got %d", len(candidateList)) - } - if candidateList[0] != "测试" { - t.Fatalf("expected fallback candidate 测试, got %#v", candidateList[0]) - } - - _, selectResp := sendProtocolMessage(t, server, "client-4", map[string]interface{}{ - "method": "filterKeyDown", - "seqNum": 4, - "keyCode": 0x31, - }) - if selectResp["commitString"] != "测试" { - t.Fatalf("expected number key to commit first fallback candidate, got %#v", selectResp) - } - if selectResp["return"] != float64(1) { - t.Fatalf("expected candidate selection return 1, got %#v", selectResp) - } - if selectResp["showCandidates"] != false { - t.Fatalf("expected candidate window to close, got %#v", selectResp) - } -} - -func TestServerHandleMessageSimplePinyinExactCandidateCommitFlow(t *testing.T) { - server := newTestServerWithSimplePinyin() - - sendProtocolMessage(t, server, "client-5", map[string]interface{}{ - "method": "init", - "seqNum": 1, - "id": testSimplePinyinGUID, - "isWindows8Above": true, - "isMetroApp": false, - "isUiLess": false, - "isConsole": false, - }) - - for i, key := range []struct { - keyCode int - charCode rune - }{ - {keyCode: 0x4E, charCode: 'n'}, - {keyCode: 0x49, charCode: 'i'}, - {keyCode: 0x48, charCode: 'h'}, - {keyCode: 0x41, charCode: 'a'}, - {keyCode: 0x4F, charCode: 'o'}, - } { - sendProtocolMessage(t, server, "client-5", map[string]interface{}{ - "method": "filterKeyDown", - "seqNum": i + 2, - "keyCode": key.keyCode, - "charCode": key.charCode, - }) - } - - _, commitResp := sendProtocolMessage(t, server, "client-5", map[string]interface{}{ - "method": "filterKeyDown", - "seqNum": 7, - "keyCode": 0x0D, - }) - if commitResp["commitString"] != "你好" { - t.Fatalf("expected enter to commit exact first candidate 你好, got %#v", commitResp) - } - if commitResp["return"] != float64(1) { - t.Fatalf("expected enter commit return 1, got %#v", commitResp) - } -} - -func TestServerHandleMessageRimeRequestResponseFlow(t *testing.T) { - server := newTestServerWithRime() - - sendProtocolMessage(t, server, "client-6", map[string]interface{}{ - "method": "init", - "seqNum": 1, - "id": testRimeGUID, - "isWindows8Above": true, - "isMetroApp": false, - "isUiLess": false, - "isConsole": false, - }) - - service, ok := server.clients["client-6"].Service.(*rimeime.IME) - if !ok { - t.Fatal("expected concrete Rime IME service") - } - if !service.BackendAvailable() { - t.Skip("native Rime backend unavailable in test environment") - } - - _, firstResp := sendProtocolMessage(t, server, "client-6", map[string]interface{}{ - "method": "filterKeyDown", - "seqNum": 2, - "keyCode": 0x4E, - "charCode": 'n', - }) - if firstResp["return"] != float64(1) { - t.Fatalf("expected first key return 1, got %#v", firstResp) - } - - _, firstKeyState := sendProtocolMessage(t, server, "client-6", map[string]interface{}{ - "method": "onKeyDown", - "seqNum": 3, - "keyCode": 0x4E, - "charCode": 'n', - }) - if firstKeyState["compositionString"] != "n" { - t.Fatalf("expected onKeyDown to expose n, got %#v", firstKeyState) - } - candidateList, ok := firstKeyState["candidateList"].([]interface{}) - if !ok || len(candidateList) == 0 { - t.Fatalf("expected prefix candidates, got %#v", firstKeyState["candidateList"]) - } - if candidateList[0] != "你" { - t.Fatalf("expected first candidate 你, got %#v", candidateList[0]) - } - - _, secondResp := sendProtocolMessage(t, server, "client-6", map[string]interface{}{ - "method": "filterKeyDown", - "seqNum": 4, - "keyCode": 0x49, - "charCode": 'i', - }) - if secondResp["return"] != float64(1) { - t.Fatalf("expected second key return 1, got %#v", secondResp) - } - - _, secondKeyState := sendProtocolMessage(t, server, "client-6", map[string]interface{}{ - "method": "onKeyDown", - "seqNum": 5, - "keyCode": 0x49, - "charCode": 'i', - }) - if secondKeyState["compositionString"] != "ni" { - t.Fatalf("expected second key to build ni, got %#v", secondKeyState) - } - secondCandidates, ok := secondKeyState["candidateList"].([]interface{}) - if !ok || len(secondCandidates) == 0 { - t.Fatalf("expected exact candidates after ni, got %#v", secondKeyState["candidateList"]) - } - if secondCandidates[1] != "呢" { - t.Fatalf("expected second candidate 呢, got %#v", secondCandidates[1]) - } - - _, selectFilterResp := sendProtocolMessage(t, server, "client-6", map[string]interface{}{ - "method": "filterKeyDown", - "seqNum": 6, - "keyCode": 0x32, - }) - if selectFilterResp["return"] != float64(1) { - t.Fatalf("expected number filter to be handled, got %#v", selectFilterResp) - } - - _, selectResp := sendProtocolMessage(t, server, "client-6", map[string]interface{}{ - "method": "onKeyDown", - "seqNum": 7, - "keyCode": 0x32, - }) - if selectResp["commitString"] != "呢" { - t.Fatalf("expected number key to commit 呢, got %#v", selectResp) - } - if selectResp["return"] != float64(1) { - t.Fatalf("expected candidate selection return 1, got %#v", selectResp) - } -} - -func TestServerHandleMessageFcitx5RequestResponseFlow(t *testing.T) { - server := newTestServerWithFcitx5() - - sendProtocolMessage(t, server, "client-7", map[string]interface{}{ - "method": "init", - "seqNum": 1, - "id": testFcitx5GUID, - "isWindows8Above": true, - "isMetroApp": false, - "isUiLess": false, - "isConsole": false, - }) - - _, firstResp := sendProtocolMessage(t, server, "client-7", map[string]interface{}{ - "method": "filterKeyDown", - "seqNum": 2, - "keyCode": 0x48, - "charCode": 'h', - }) - if firstResp["compositionString"] != "ha" { - t.Fatalf("expected first key to build ha, got %#v", firstResp) - } - if firstResp["return"] != float64(1) { - t.Fatalf("expected first key return 1, got %#v", firstResp) - } - candidateList, ok := firstResp["candidateList"].([]interface{}) - if !ok { - t.Fatalf("expected candidate list array, got %#v", firstResp["candidateList"]) - } - if len(candidateList) != 5 { - t.Fatalf("expected 5 candidates, got %d", len(candidateList)) - } - if candidateList[2] != "喝" { - t.Fatalf("expected third candidate 喝, got %#v", candidateList[2]) - } - - _, selectResp := sendProtocolMessage(t, server, "client-7", map[string]interface{}{ - "method": "onKeyDown", - "seqNum": 3, - "keyCode": 0x33, - "candidateList": []string{"哈", "呵", "喝", "和", "河"}, - }) - if selectResp["commitString"] != "喝" { - t.Fatalf("expected number key to commit 喝, got %#v", selectResp) - } - if selectResp["return"] != float64(1) { - t.Fatalf("expected candidate selection return 1, got %#v", selectResp) - } -} diff --git a/go-backend/server_test.go b/go-backend/server_test.go deleted file mode 100644 index 7c99507e0..000000000 --- a/go-backend/server_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package main - -import ( - "testing" - - "github.com/EasyIME/pime-go/pime" -) - -func TestConvertResponseIncludesClearedCompositionState(t *testing.T) { - server := NewServer() - resp := pime.NewResponse(1, true) - resp.ReturnValue = 1 - - got := server.convertResponse(resp) - - if value, ok := got["compositionString"]; !ok || value != "" { - t.Fatalf("expected empty compositionString, got %#v", got["compositionString"]) - } - if value, ok := got["candidateList"]; !ok { - t.Fatalf("expected candidateList in response, got %#v", got) - } else if list, ok := value.([]string); !ok || len(list) != 0 { - t.Fatalf("expected empty candidateList, got %#v", value) - } - if value, ok := got["showCandidates"]; !ok || value.(bool) { - t.Fatalf("expected showCandidates=false, got %#v", got["showCandidates"]) - } - if value, ok := got["selStart"]; !ok || value.(int) != 0 { - t.Fatalf("expected selStart=0, got %#v", got["selStart"]) - } - if value, ok := got["selEnd"]; !ok || value.(int) != 0 { - t.Fatalf("expected selEnd=0, got %#v", got["selEnd"]) - } -} - -func TestConvertResponseUsesReturnDataWhenPresent(t *testing.T) { - server := NewServer() - resp := pime.NewResponse(2, true) - resp.ReturnValue = 1 - resp.ReturnData = []map[string]interface{}{ - {"id": 1, "text": "中文 → 西文"}, - } - - got := server.convertResponse(resp) - - items, ok := got["return"].([]map[string]interface{}) - if !ok || len(items) != 1 { - t.Fatalf("expected menu return data, got %#v", got["return"]) - } -}