Implement native Windows KSwitcher with configurable hotkeys and clipboard history
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
#include "app.h"
|
||||
#include <iostream>
|
||||
|
||||
namespace
|
||||
{
|
||||
ks::App *app{};
|
||||
HWND host{}, field{};
|
||||
ks::Clip saved;
|
||||
bool captured = false, failed = false;
|
||||
int stage = 0, ru = -1, en = -1;
|
||||
ULONGLONG deadline{};
|
||||
std::wstring sentinel = L"KSwitcher integration clipboard sentinel";
|
||||
void require(bool ok, const char *name)
|
||||
{
|
||||
if (!ok)
|
||||
throw std::runtime_error(name);
|
||||
std::cout << "PASS " << name << '\n';
|
||||
}
|
||||
std::wstring text()
|
||||
{
|
||||
wchar_t buffer[2048]{};
|
||||
GetWindowTextW(field, buffer, 2048);
|
||||
return buffer;
|
||||
}
|
||||
void prepare(const wchar_t *value, bool select)
|
||||
{
|
||||
app->resetInput();
|
||||
SetWindowTextW(field, value);
|
||||
ks::activateWindow(host);
|
||||
SetFocus(field);
|
||||
int end = GetWindowTextLengthW(field);
|
||||
SendMessageW(field, EM_SETSEL, select ? 0 : end, end);
|
||||
if (GetForegroundWindow() != host)
|
||||
throw std::runtime_error("Test editor could not acquire foreground; no input was sent");
|
||||
}
|
||||
void record(const std::wstring &keys)
|
||||
{
|
||||
std::lock_guard lock(app->stateMutex);
|
||||
for (wchar_t c : keys)
|
||||
app->run.record(c, host, app->layouts[en].handle, GetTickCount64());
|
||||
}
|
||||
void finish()
|
||||
{
|
||||
KillTimer(host, 1);
|
||||
if (captured)
|
||||
{
|
||||
try
|
||||
{
|
||||
app->ownSequence = ks::writeClipboard(app->window, saved);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
failed = true;
|
||||
}
|
||||
}
|
||||
DestroyWindow(host);
|
||||
PostMessageW(app->window, WM_CLOSE, 0, 0);
|
||||
}
|
||||
LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
{
|
||||
if (msg == WM_TIMER)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (GetTickCount64() > deadline)
|
||||
throw std::runtime_error("Native integration timed out");
|
||||
if (app->busy)
|
||||
return 0;
|
||||
switch (stage++)
|
||||
{
|
||||
case 0:
|
||||
saved = ks::readClipboard(app->window, 64 * 1024 * 1024);
|
||||
captured = true;
|
||||
app->ownSequence = ks::writeClipboard(app->window, ks::textClip(sentinel));
|
||||
prepare(L"ghbdtn", true);
|
||||
app->transform(ks::Direct + ru);
|
||||
break;
|
||||
case 1:
|
||||
std::cout << "Observed editor: " << ks::utf8(text()) << "; busy=" << app->busy
|
||||
<< "; foreground=" << (GetForegroundWindow() == host) << "\n";
|
||||
require(text() == L"привет", "selection conversion through actual Ctrl+C / Ctrl+V");
|
||||
require(ks::readClipboard(app->window).text() == sentinel, "clipboard restored");
|
||||
app->transform(ks::Undo);
|
||||
break;
|
||||
case 2:
|
||||
require(text() == L"ghbdtn", "verified undo");
|
||||
prepare(L"hello world", true);
|
||||
app->transform(ks::Case);
|
||||
break;
|
||||
case 3:
|
||||
require(text() == L"Hello World", "case conversion");
|
||||
app->transform(ks::Case);
|
||||
break;
|
||||
case 4:
|
||||
require(text() == L"HELLO WORLD", "consecutive command reselects verified previous result");
|
||||
prepare(L"OLDghbdtn", false);
|
||||
record(L"ghbdtn");
|
||||
app->transform(ks::Direct + ru);
|
||||
break;
|
||||
case 5:
|
||||
std::cout << "Recent observed: " << ks::utf8(text()) << "\n";
|
||||
require(text() == L"OLDпривет", "recent-input conversion preserves adjacent older text");
|
||||
prepare(L"OLDabcdef", false);
|
||||
record(L"ghbdtn");
|
||||
app->transform(ks::Direct + ru);
|
||||
break;
|
||||
case 6:
|
||||
require(text() == L"OLDabcdef", "recent-input mismatch refuses replacement");
|
||||
app->settings.snippets = {{L";addr", L"LINE1\r\n{cursor}LINE2"}};
|
||||
prepare(L"prefix ;addr", false);
|
||||
record(L";addr");
|
||||
app->transform(0, 0);
|
||||
break;
|
||||
case 7:
|
||||
{
|
||||
require(text() == L"prefix LINE1\r\nLINE2", "multiline snippet expansion");
|
||||
DWORD start{}, end{};
|
||||
SendMessageW(field, EM_GETSEL, (WPARAM)&start, (LPARAM)&end);
|
||||
require(start == end && start == 14, "verified snippet cursor placement");
|
||||
require(ks::readClipboard(app->window).text() == sentinel,
|
||||
"clipboard restored after repeated copy transactions");
|
||||
finish();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
failed = true;
|
||||
std::cerr << "FAIL " << e.what() << '\n';
|
||||
finish();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (msg == WM_CLOSE)
|
||||
{
|
||||
failed = true;
|
||||
finish();
|
||||
return 0;
|
||||
}
|
||||
return DefWindowProcW(w, msg, wp, lp);
|
||||
}
|
||||
} // namespace
|
||||
int main()
|
||||
{
|
||||
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||
auto root = std::filesystem::temp_directory_path() /
|
||||
(L"KSwitcher-integration-" + std::to_wstring(GetCurrentProcessId()));
|
||||
try
|
||||
{
|
||||
{
|
||||
ks::App instance(GetModuleHandleW(nullptr), false, root);
|
||||
app = &instance;
|
||||
app->settings.history = false;
|
||||
app->settings.switchLayout = false;
|
||||
for (int i = 0; i < (int)app->layouts.size(); ++i)
|
||||
{
|
||||
auto language = LOWORD((uintptr_t)app->layouts[i].handle);
|
||||
if (language == 0x409)
|
||||
en = i;
|
||||
if (language == 0x419)
|
||||
ru = i;
|
||||
}
|
||||
if (en < 0 || ru < 0)
|
||||
throw std::runtime_error(
|
||||
"Integration test requires installed English US and Russian layouts");
|
||||
WNDCLASSW cls{};
|
||||
cls.hInstance = GetModuleHandleW(nullptr);
|
||||
cls.lpfnWndProc = procedure;
|
||||
cls.lpszClassName = L"KSwitcher.TestEditor";
|
||||
cls.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
|
||||
RegisterClassW(&cls);
|
||||
host = CreateWindowW(cls.lpszClassName, L"KSwitcher — automated test editor",
|
||||
WS_OVERLAPPEDWINDOW | WS_VISIBLE, 100, 100, 650, 320, nullptr, nullptr,
|
||||
cls.hInstance, nullptr);
|
||||
field =
|
||||
CreateWindowW(L"EDIT", L"", WS_CHILD | WS_VISIBLE | WS_BORDER | ES_MULTILINE | ES_WANTRETURN,
|
||||
20, 20, 590, 230, host, nullptr, cls.hInstance, nullptr);
|
||||
deadline = GetTickCount64() + 30000;
|
||||
SetTimer(host, 1, 100, nullptr);
|
||||
app->execute();
|
||||
}
|
||||
std::filesystem::remove_all(root);
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
std::cerr << e.what() << '\n';
|
||||
failed = true;
|
||||
}
|
||||
CoUninitialize();
|
||||
return failed ? 1 : 0;
|
||||
}
|
||||
Reference in New Issue
Block a user