diff --git a/openless-all/app/windows-ime/OpenLessIme.sln b/openless-all/app/windows-ime/OpenLessIme.sln
new file mode 100644
index 00000000..a2fc9f08
--- /dev/null
+++ b/openless-all/app/windows-ime/OpenLessIme.sln
@@ -0,0 +1,21 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{BC8A1FFA-BEE3-4634-8014-F334798102B3}") = "OpenLessIme", "OpenLessIme.vcxproj", "{F45287CB-1F31-4D7C-9B62-5748C2F8C9B0}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|x64 = Debug|x64
+ Release|x64 = Release|x64
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {F45287CB-1F31-4D7C-9B62-5748C2F8C9B0}.Debug|x64.ActiveCfg = Debug|x64
+ {F45287CB-1F31-4D7C-9B62-5748C2F8C9B0}.Debug|x64.Build.0 = Debug|x64
+ {F45287CB-1F31-4D7C-9B62-5748C2F8C9B0}.Release|x64.ActiveCfg = Release|x64
+ {F45287CB-1F31-4D7C-9B62-5748C2F8C9B0}.Release|x64.Build.0 = Release|x64
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/openless-all/app/windows-ime/OpenLessIme.vcxproj b/openless-all/app/windows-ime/OpenLessIme.vcxproj
new file mode 100644
index 00000000..0275bfae
--- /dev/null
+++ b/openless-all/app/windows-ime/OpenLessIme.vcxproj
@@ -0,0 +1,91 @@
+
+
+
+
+ Debug
+ x64
+
+
+ Release
+ x64
+
+
+
+ 17.0
+ Win32Proj
+ {F45287CB-1F31-4D7C-9B62-5748C2F8C9B0}
+ OpenLessIme
+ 10.0
+
+
+
+ DynamicLibrary
+ true
+ v143
+ Unicode
+
+
+ DynamicLibrary
+ false
+ v143
+ true
+ Unicode
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Level4
+ true
+ WIN32;_WINDOWS;_USRDLL;OPENLESSIME_EXPORTS;%(PreprocessorDefinitions)
+ true
+ stdcpp17
+
+
+ Windows
+ msctf.lib;ole32.lib;uuid.lib;advapi32.lib;%(AdditionalDependencies)
+
+
+
+
+ Level4
+ true
+ true
+ true
+ WIN32;NDEBUG;_WINDOWS;_USRDLL;OPENLESSIME_EXPORTS;%(PreprocessorDefinitions)
+ true
+ stdcpp17
+
+
+ Windows
+ true
+ true
+ msctf.lib;ole32.lib;uuid.lib;advapi32.lib;%(AdditionalDependencies)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/openless-all/app/windows-ime/src/class_factory.cpp b/openless-all/app/windows-ime/src/class_factory.cpp
new file mode 100644
index 00000000..7cfb095e
--- /dev/null
+++ b/openless-all/app/windows-ime/src/class_factory.cpp
@@ -0,0 +1,75 @@
+#include "class_factory.h"
+
+#include
+#include
+
+#include "text_service.h"
+
+extern LONG g_lock_count;
+extern LONG g_object_count;
+
+OpenLessClassFactory::OpenLessClassFactory() {
+ InterlockedIncrement(&g_object_count);
+}
+
+OpenLessClassFactory::~OpenLessClassFactory() {
+ InterlockedDecrement(&g_object_count);
+}
+
+STDMETHODIMP OpenLessClassFactory::QueryInterface(REFIID iid, void** object) {
+ if (object == nullptr) {
+ return E_POINTER;
+ }
+ *object = nullptr;
+
+ if (iid == IID_IUnknown || iid == IID_IClassFactory) {
+ *object = static_cast(this);
+ AddRef();
+ return S_OK;
+ }
+
+ return E_NOINTERFACE;
+}
+
+STDMETHODIMP_(ULONG) OpenLessClassFactory::AddRef() {
+ return static_cast(InterlockedIncrement(&ref_count_));
+}
+
+STDMETHODIMP_(ULONG) OpenLessClassFactory::Release() {
+ const ULONG count = static_cast(InterlockedDecrement(&ref_count_));
+ if (count == 0) {
+ delete this;
+ }
+ return count;
+}
+
+STDMETHODIMP OpenLessClassFactory::CreateInstance(IUnknown* outer,
+ REFIID iid,
+ void** object) {
+ if (object == nullptr) {
+ return E_POINTER;
+ }
+ *object = nullptr;
+
+ if (outer != nullptr) {
+ return CLASS_E_NOAGGREGATION;
+ }
+
+ auto* service = new (std::nothrow) OpenLessTextService();
+ if (service == nullptr) {
+ return E_OUTOFMEMORY;
+ }
+
+ const HRESULT hr = service->QueryInterface(iid, object);
+ service->Release();
+ return hr;
+}
+
+STDMETHODIMP OpenLessClassFactory::LockServer(BOOL lock) {
+ if (lock) {
+ InterlockedIncrement(&g_lock_count);
+ } else {
+ InterlockedDecrement(&g_lock_count);
+ }
+ return S_OK;
+}
diff --git a/openless-all/app/windows-ime/src/class_factory.h b/openless-all/app/windows-ime/src/class_factory.h
new file mode 100644
index 00000000..bb2de3e9
--- /dev/null
+++ b/openless-all/app/windows-ime/src/class_factory.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#include
+
+class OpenLessClassFactory final : public IClassFactory {
+ public:
+ OpenLessClassFactory();
+ OpenLessClassFactory(const OpenLessClassFactory&) = delete;
+ OpenLessClassFactory& operator=(const OpenLessClassFactory&) = delete;
+ ~OpenLessClassFactory() override;
+
+ STDMETHODIMP QueryInterface(REFIID iid, void** object) override;
+ STDMETHODIMP_(ULONG) AddRef() override;
+ STDMETHODIMP_(ULONG) Release() override;
+ STDMETHODIMP CreateInstance(IUnknown* outer, REFIID iid, void** object) override;
+ STDMETHODIMP LockServer(BOOL lock) override;
+
+ private:
+ LONG ref_count_ = 1;
+};
diff --git a/openless-all/app/windows-ime/src/dllmain.cpp b/openless-all/app/windows-ime/src/dllmain.cpp
new file mode 100644
index 00000000..84476ba2
--- /dev/null
+++ b/openless-all/app/windows-ime/src/dllmain.cpp
@@ -0,0 +1,54 @@
+#include
+
+#include
+
+#include "class_factory.h"
+#include "guids.h"
+#include "registry.h"
+
+HINSTANCE g_module = nullptr;
+LONG g_lock_count = 0;
+LONG g_object_count = 0;
+
+BOOL APIENTRY DllMain(HINSTANCE instance, DWORD reason, LPVOID reserved) {
+ UNREFERENCED_PARAMETER(reserved);
+
+ if (reason == DLL_PROCESS_ATTACH) {
+ g_module = instance;
+ DisableThreadLibraryCalls(instance);
+ }
+
+ return TRUE;
+}
+
+STDAPI DllCanUnloadNow() {
+ return (g_lock_count == 0 && g_object_count == 0) ? S_OK : S_FALSE;
+}
+
+STDAPI DllGetClassObject(REFCLSID clsid, REFIID iid, void** object) {
+ if (object == nullptr) {
+ return E_POINTER;
+ }
+ *object = nullptr;
+
+ if (clsid != CLSID_OpenLessTextService) {
+ return CLASS_E_CLASSNOTAVAILABLE;
+ }
+
+ auto* factory = new (std::nothrow) OpenLessClassFactory();
+ if (factory == nullptr) {
+ return E_OUTOFMEMORY;
+ }
+
+ const HRESULT hr = factory->QueryInterface(iid, object);
+ factory->Release();
+ return hr;
+}
+
+STDAPI DllRegisterServer() {
+ return RegisterOpenLessTextService(g_module);
+}
+
+STDAPI DllUnregisterServer() {
+ return UnregisterOpenLessTextService();
+}
diff --git a/openless-all/app/windows-ime/src/guids.h b/openless-all/app/windows-ime/src/guids.h
new file mode 100644
index 00000000..a22782bf
--- /dev/null
+++ b/openless-all/app/windows-ime/src/guids.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include
+#include
+
+inline constexpr CLSID CLSID_OpenLessTextService = {
+ 0x6b9f3f4f,
+ 0x5ee7,
+ 0x42d6,
+ {0x9c, 0x61, 0x9f, 0x80, 0xb0, 0x3a, 0x5d, 0x7d},
+};
+
+inline constexpr GUID GUID_OpenLessProfile = {
+ 0x9b5f5e04,
+ 0x23f6,
+ 0x47da,
+ {0x9a, 0x26, 0xd2, 0x21, 0xf6, 0xc3, 0xf0, 0x2e},
+};
+
+inline constexpr wchar_t kOpenLessImeName[] = L"OpenLess Voice Input";
+inline constexpr LANGID kOpenLessLangId = 0x0804;
diff --git a/openless-all/app/windows-ime/src/registry.cpp b/openless-all/app/windows-ime/src/registry.cpp
new file mode 100644
index 00000000..6a5e5656
--- /dev/null
+++ b/openless-all/app/windows-ime/src/registry.cpp
@@ -0,0 +1,176 @@
+#include "registry.h"
+
+#include
+#include
+
+#include "guids.h"
+
+namespace {
+
+constexpr wchar_t kClsidKey[] =
+ L"Software\\Classes\\CLSID\\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D}";
+constexpr wchar_t kInprocServer32Key[] =
+ L"Software\\Classes\\CLSID\\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D}\\InprocServer32";
+
+HRESULT HResultFromWin32Error(LSTATUS status) {
+ return status == ERROR_SUCCESS ? S_OK : HRESULT_FROM_WIN32(status);
+}
+
+HRESULT SetStringValue(HKEY key, const wchar_t* name, const wchar_t* value) {
+ const auto byte_count =
+ static_cast((wcslen(value) + 1) * sizeof(wchar_t));
+ return HResultFromWin32Error(
+ RegSetValueExW(key, name, 0, REG_SZ,
+ reinterpret_cast(value), byte_count));
+}
+
+HRESULT RegisterComServer(HINSTANCE module) {
+ wchar_t module_path[MAX_PATH] = {};
+ const DWORD path_len =
+ GetModuleFileNameW(module, module_path, ARRAYSIZE(module_path));
+ if (path_len == 0) {
+ return HRESULT_FROM_WIN32(GetLastError());
+ }
+ if (path_len == ARRAYSIZE(module_path)) {
+ return HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER);
+ }
+
+ HKEY clsid_key = nullptr;
+ LSTATUS status =
+ RegCreateKeyExW(HKEY_CURRENT_USER, kClsidKey, 0, nullptr,
+ REG_OPTION_NON_VOLATILE, KEY_WRITE, nullptr, &clsid_key,
+ nullptr);
+ HRESULT hr = HResultFromWin32Error(status);
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ hr = SetStringValue(clsid_key, nullptr, kOpenLessImeName);
+ RegCloseKey(clsid_key);
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ HKEY inproc_key = nullptr;
+ status = RegCreateKeyExW(HKEY_CURRENT_USER, kInprocServer32Key, 0, nullptr,
+ REG_OPTION_NON_VOLATILE, KEY_WRITE, nullptr,
+ &inproc_key, nullptr);
+ hr = HResultFromWin32Error(status);
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ hr = SetStringValue(inproc_key, nullptr, module_path);
+ if (SUCCEEDED(hr)) {
+ hr = SetStringValue(inproc_key, L"ThreadingModel", L"Apartment");
+ }
+ RegCloseKey(inproc_key);
+ return hr;
+}
+
+HRESULT DeleteComServerRegistration() {
+ const LSTATUS status = RegDeleteTreeW(HKEY_CURRENT_USER, kClsidKey);
+ if (status == ERROR_FILE_NOT_FOUND) {
+ return S_OK;
+ }
+ return HResultFromWin32Error(status);
+}
+
+class ScopedComInit {
+ public:
+ ScopedComInit() : hr_(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED)) {}
+ ScopedComInit(const ScopedComInit&) = delete;
+ ScopedComInit& operator=(const ScopedComInit&) = delete;
+ ~ScopedComInit() {
+ if (hr_ == S_OK || hr_ == S_FALSE) {
+ CoUninitialize();
+ }
+ }
+
+ HRESULT hr() const {
+ return hr_ == RPC_E_CHANGED_MODE ? S_OK : hr_;
+ }
+
+ private:
+ HRESULT hr_;
+};
+
+HRESULT CreateProfiles(ITfInputProcessorProfiles** profiles) {
+ return CoCreateInstance(CLSID_TF_InputProcessorProfiles, nullptr,
+ CLSCTX_INPROC_SERVER,
+ IID_ITfInputProcessorProfiles,
+ reinterpret_cast(profiles));
+}
+
+HRESULT RegisterLanguageProfile() {
+ ScopedComInit com;
+ HRESULT hr = com.hr();
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ ITfInputProcessorProfiles* profiles = nullptr;
+ hr = CreateProfiles(&profiles);
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ hr = profiles->Register(CLSID_OpenLessTextService);
+ if (SUCCEEDED(hr)) {
+ hr = profiles->AddLanguageProfile(
+ CLSID_OpenLessTextService, kOpenLessLangId, GUID_OpenLessProfile,
+ const_cast(kOpenLessImeName),
+ static_cast(ARRAYSIZE(kOpenLessImeName) - 1), nullptr, 0, 0);
+ }
+ if (SUCCEEDED(hr)) {
+ hr = profiles->EnableLanguageProfile(CLSID_OpenLessTextService,
+ kOpenLessLangId, GUID_OpenLessProfile,
+ TRUE);
+ }
+
+ profiles->Release();
+ return hr;
+}
+
+HRESULT UnregisterLanguageProfile() {
+ ScopedComInit com;
+ HRESULT hr = com.hr();
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ ITfInputProcessorProfiles* profiles = nullptr;
+ hr = CreateProfiles(&profiles);
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ hr = profiles->Unregister(CLSID_OpenLessTextService);
+ profiles->Release();
+ return hr;
+}
+
+} // namespace
+
+HRESULT RegisterOpenLessTextService(HINSTANCE module) {
+ if (module == nullptr) {
+ return E_INVALIDARG;
+ }
+
+ HRESULT hr = RegisterComServer(module);
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ hr = RegisterLanguageProfile();
+ if (FAILED(hr)) {
+ DeleteComServerRegistration();
+ }
+ return hr;
+}
+
+HRESULT UnregisterOpenLessTextService() {
+ const HRESULT profile_hr = UnregisterLanguageProfile();
+ const HRESULT registry_hr = DeleteComServerRegistration();
+ return FAILED(profile_hr) ? profile_hr : registry_hr;
+}
diff --git a/openless-all/app/windows-ime/src/registry.h b/openless-all/app/windows-ime/src/registry.h
new file mode 100644
index 00000000..74e4ab8d
--- /dev/null
+++ b/openless-all/app/windows-ime/src/registry.h
@@ -0,0 +1,6 @@
+#pragma once
+
+#include
+
+HRESULT RegisterOpenLessTextService(HINSTANCE module);
+HRESULT UnregisterOpenLessTextService();
diff --git a/openless-all/app/windows-ime/src/resource.rc b/openless-all/app/windows-ime/src/resource.rc
new file mode 100644
index 00000000..c27e46d2
--- /dev/null
+++ b/openless-all/app/windows-ime/src/resource.rc
@@ -0,0 +1,33 @@
+#include
+
+1 VERSIONINFO
+FILEVERSION 0,1,0,0
+PRODUCTVERSION 0,1,0,0
+FILEFLAGSMASK 0x3fL
+#ifdef _DEBUG
+FILEFLAGS 0x1L
+#else
+FILEFLAGS 0x0L
+#endif
+FILEOS 0x40004L
+FILETYPE 0x2L
+FILESUBTYPE 0x0L
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904b0"
+ BEGIN
+ VALUE "CompanyName", "OpenLess"
+ VALUE "FileDescription", "OpenLess Voice Input TSF IME"
+ VALUE "FileVersion", "0.1.0.0"
+ VALUE "InternalName", "OpenLessIme.dll"
+ VALUE "OriginalFilename", "OpenLessIme.dll"
+ VALUE "ProductName", "OpenLess Voice Input"
+ VALUE "ProductVersion", "0.1.0.0"
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x409, 1200
+ END
+END
diff --git a/openless-all/app/windows-ime/src/text_service.cpp b/openless-all/app/windows-ime/src/text_service.cpp
new file mode 100644
index 00000000..7a0caa79
--- /dev/null
+++ b/openless-all/app/windows-ime/src/text_service.cpp
@@ -0,0 +1,95 @@
+#include "text_service.h"
+
+extern LONG g_object_count;
+
+OpenLessTextService::OpenLessTextService() {
+ InterlockedIncrement(&g_object_count);
+}
+
+OpenLessTextService::~OpenLessTextService() {
+ Deactivate();
+ InterlockedDecrement(&g_object_count);
+}
+
+STDMETHODIMP OpenLessTextService::QueryInterface(REFIID iid, void** object) {
+ if (object == nullptr) {
+ return E_POINTER;
+ }
+ *object = nullptr;
+
+ if (iid == IID_IUnknown || iid == IID_ITfTextInputProcessor ||
+ iid == IID_ITfTextInputProcessorEx) {
+ *object = static_cast(this);
+ AddRef();
+ return S_OK;
+ }
+
+ return E_NOINTERFACE;
+}
+
+STDMETHODIMP_(ULONG) OpenLessTextService::AddRef() {
+ return static_cast(InterlockedIncrement(&ref_count_));
+}
+
+STDMETHODIMP_(ULONG) OpenLessTextService::Release() {
+ const ULONG count = static_cast(InterlockedDecrement(&ref_count_));
+ if (count == 0) {
+ delete this;
+ }
+ return count;
+}
+
+STDMETHODIMP OpenLessTextService::Activate(ITfThreadMgr* thread_mgr,
+ TfClientId client_id) {
+ return ActivateEx(thread_mgr, client_id, 0);
+}
+
+STDMETHODIMP OpenLessTextService::ActivateEx(ITfThreadMgr* thread_mgr,
+ TfClientId client_id,
+ DWORD flags) {
+ UNREFERENCED_PARAMETER(flags);
+
+ if (thread_mgr == nullptr) {
+ return E_INVALIDARG;
+ }
+
+ Deactivate();
+
+ thread_mgr_ = thread_mgr;
+ thread_mgr_->AddRef();
+ client_id_ = client_id;
+
+ const HRESULT hr = StartIpcServer();
+ if (FAILED(hr)) {
+ Deactivate();
+ return hr;
+ }
+
+ return S_OK;
+}
+
+STDMETHODIMP OpenLessTextService::Deactivate() {
+ StopIpcServer();
+
+ if (thread_mgr_ != nullptr) {
+ thread_mgr_->Release();
+ thread_mgr_ = nullptr;
+ }
+ client_id_ = TF_CLIENTID_NULL;
+
+ return S_OK;
+}
+
+HRESULT OpenLessTextService::SubmitTextFromPipe(
+ const std::wstring& session_id,
+ const std::wstring& text) {
+ UNREFERENCED_PARAMETER(session_id);
+ UNREFERENCED_PARAMETER(text);
+ return E_NOTIMPL;
+}
+
+HRESULT OpenLessTextService::StartIpcServer() {
+ return S_OK;
+}
+
+void OpenLessTextService::StopIpcServer() {}
diff --git a/openless-all/app/windows-ime/src/text_service.h b/openless-all/app/windows-ime/src/text_service.h
new file mode 100644
index 00000000..ac1d8576
--- /dev/null
+++ b/openless-all/app/windows-ime/src/text_service.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include
+#include
+#include
+
+class OpenLessTextService final : public ITfTextInputProcessorEx {
+ public:
+ OpenLessTextService();
+ OpenLessTextService(const OpenLessTextService&) = delete;
+ OpenLessTextService& operator=(const OpenLessTextService&) = delete;
+ ~OpenLessTextService() override;
+
+ STDMETHODIMP QueryInterface(REFIID iid, void** object) override;
+ STDMETHODIMP_(ULONG) AddRef() override;
+ STDMETHODIMP_(ULONG) Release() override;
+
+ STDMETHODIMP Activate(ITfThreadMgr* thread_mgr, TfClientId client_id) override;
+ STDMETHODIMP Deactivate() override;
+ STDMETHODIMP ActivateEx(ITfThreadMgr* thread_mgr,
+ TfClientId client_id,
+ DWORD flags) override;
+
+ HRESULT SubmitTextFromPipe(const std::wstring& session_id,
+ const std::wstring& text);
+
+ private:
+ HRESULT StartIpcServer();
+ void StopIpcServer();
+
+ LONG ref_count_ = 1;
+ ITfThreadMgr* thread_mgr_ = nullptr;
+ TfClientId client_id_ = TF_CLIENTID_NULL;
+};