Implement native Windows KSwitcher with configurable hotkeys and clipboard history

This commit is contained in:
2026-10-27 22:14:06 +02:00
commit f683e4178c
37 changed files with 31288 additions and 0 deletions
+467
View File
@@ -0,0 +1,467 @@
#include "core.h"
#include <algorithm>
#include <cwctype>
#include <map>
#include <stdexcept>
namespace ks
{
int HotkeyHook::update(UINT key, bool down, UINT modifiers,
const std::vector<std::pair<int, Hotkey>> &bindings)
{
if (key >= 256)
return 0;
if (pressed[key])
{
if (!down)
pressed[key] = false;
return -1;
}
if (down)
for (const auto &[command, hotkey] : bindings)
if (hotkey.key == key && hotkey.modifiers == modifiers)
{
pressed[key] = true;
return command;
}
return 0;
}
bool letter(wchar_t c)
{
WORD type{};
GetStringTypeW(CT_CTYPE1, &c, 1, &type);
return (type & C1_ALPHA) != 0;
}
std::wstring lower(std::wstring s)
{
if (!s.empty())
CharLowerBuffW(s.data(), (DWORD)s.size());
return s;
}
std::wstring upper(std::wstring s)
{
if (!s.empty())
CharUpperBuffW(s.data(), (DWORD)s.size());
return s;
}
int Layout::code(wchar_t c) const
{
auto p = normal.find(c);
if (p != normal.npos && c)
return (int)p;
p = shifted.find(c);
return p == shifted.npos || !c ? -1 : (int)(p + normal.size());
}
wchar_t Layout::at(int c) const
{
if (c < 0)
return 0;
return c < (int)normal.size() ? normal[c]
: c < (int)(normal.size() + shifted.size()) ? shifted[c - normal.size()]
: 0;
}
bool Layout::hasLetter(wchar_t c) const
{
return letter(c) && code(c) >= 0;
}
std::wstring convert(const std::wstring &text, const Layout &from, const Layout &to, bool force)
{
std::wstring result;
for (size_t i = 0; i < text.size();)
{
if (iswspace(text[i]))
{
result += text[i++];
continue;
}
size_t end = i;
bool source = false, other = false;
while (end < text.size() && !iswspace(text[end]))
{
wchar_t c = text[end++];
if (!letter(c))
continue;
if (from.hasLetter(c) && !to.hasLetter(c))
source = true;
else
other = true;
}
for (; i < end; ++i)
{
wchar_t mapped = (!force && !source && other) ? 0 : to.at(from.code(text[i]));
result += mapped ? mapped : text[i];
}
}
return result;
}
int detect(const std::wstring &text, const std::vector<Layout> &layouts, int preferred)
{
int best = -1, score = 0;
auto check = [&](int i)
{
if (i < 0 || i >= (int)layouts.size() || !layouts[i].convertible)
return;
int n =
(int)std::count_if(text.begin(), text.end(), [&](wchar_t c) { return layouts[i].hasLetter(c); });
if (n > score)
{
score = n;
best = i;
}
};
check(preferred);
for (int i = 0; i < (int)layouts.size(); ++i)
check(i);
return best;
}
std::wstring convertTo(const std::wstring &text, const std::vector<Layout> &layouts, int target, int hint)
{
if (target < 0 || target >= (int)layouts.size() || !layouts[target].convertible)
return text;
const auto &to = layouts[target];
if (hint >= 0 && hint < (int)layouts.size() && layouts[hint].convertible &&
std::all_of(
text.begin(), text.end(), [&](wchar_t c) { return !letter(c) || layouts[hint].code(c) >= 0; }))
return hint == target ? text : convert(text, layouts[hint], to, true);
int best = -1, score = 0;
for (int i = 0; i < (int)layouts.size(); ++i)
if (i != target && layouts[i].convertible)
{
int n = (int)std::count_if(text.begin(), text.end(), [&](wchar_t c)
{ return layouts[i].hasLetter(c) && !to.hasLetter(c); });
if (n > score)
{
score = n;
best = i;
}
}
if (best < 0 && detect(text, layouts) < 0)
{
best = hint;
if (best < 0)
for (int i = 0; i < (int)layouts.size(); ++i)
if (i != target && layouts[i].convertible)
{
best = i;
break;
}
}
return best < 0 ? text : convert(text, layouts[best], to);
}
std::wstring cycleCase(const std::wstring &text)
{
if (text == upper(text))
return lower(text);
if (text != lower(text))
return upper(text);
auto out = lower(text);
bool start = true;
for (auto &c : out)
{
if (letter(c))
{
if (start)
c = upper(std::wstring(1, c))[0];
start = false;
}
else
start = !iswdigit(c) && c != L'\'' && c != L'’';
}
return out;
}
std::wstring transliterate(const std::wstring &text)
{
static const std::wstring ru = L"абвгдеёжзийклмнопрстуфхцчшщъыьэюя";
static const std::vector<std::wstring> lat = {L"a", L"b", L"v", L"g", L"d", L"e", L"yo", L"zh", L"z",
L"i", L"y", L"k", L"l", L"m", L"n", L"o", L"p", L"r",
L"s", L"t", L"u", L"f", L"kh", L"ts", L"ch", L"sh", L"shch",
L"", L"y", L"", L"e", L"yu", L"ya"};
bool cyr = std::any_of(text.begin(), text.end(), [](wchar_t c) { return c >= 0x400 && c <= 0x4ff; });
std::wstring out, lo = lower(text);
if (cyr)
{
bool uk = lo.find_first_of(L"ґіїє") != lo.npos;
const std::map<wchar_t, std::wstring> overrides = {{L'г', L"h"}, {L'ґ', L"g"}, {L'и', L"y"},
{L'і', L"i"}, {L'ї', L"yi"}, {L'є', L"ye"},
{L'\'', L""}, {L'’', L""}};
for (size_t i = 0; i < text.size(); ++i)
{
std::wstring m;
auto p = ru.find(lo[i]);
auto u = overrides.find(lo[i]);
if (uk && u != overrides.end())
{
m = u->second;
if (i && letter(text[i - 1]))
{
if (lo[i] == L'ї')
m = L"i";
if (lo[i] == L'є')
m = L"ie";
}
}
else if (p != ru.npos)
m = lat[p];
else
{
out += text[i];
continue;
}
if (text[i] != lo[i] && !m.empty())
{
bool all =
(i + 1 < text.size() && text[i + 1] != lo[i + 1]) || (i > 0 && text[i - 1] != lo[i - 1]);
if (all)
m = upper(m);
else
m[0] = upper(m.substr(0, 1))[0];
}
out += m;
}
}
else
{
static const std::vector<std::pair<std::wstring, std::wstring>> reverse = {
{L"shch", L"щ"}, {L"zh", L"ж"}, {L"kh", L"х"}, {L"ts", L"ц"}, {L"ch", L"ч"}, {L"sh", L"ш"},
{L"yo", L"ё"}, {L"yu", L"ю"}, {L"ya", L"я"}, {L"ye", L"е"}, {L"a", L"а"}, {L"b", L"б"},
{L"v", L"в"}, {L"w", L"в"}, {L"g", L"г"}, {L"d", L"д"}, {L"e", L"е"}, {L"z", L"з"},
{L"i", L"и"}, {L"y", L"й"}, {L"k", L"к"}, {L"q", L"к"}, {L"l", L"л"}, {L"m", L"м"},
{L"n", L"н"}, {L"o", L"о"}, {L"p", L"п"}, {L"r", L"р"}, {L"s", L"с"}, {L"t", L"т"},
{L"u", L"у"}, {L"f", L"ф"}, {L"h", L"х"}, {L"c", L"к"}, {L"x", L"кс"}, {L"j", L"дж"}};
for (size_t i = 0; i < text.size();)
{
bool found = false;
for (const auto &[a, b] : reverse)
if (lo.compare(i, a.size(), a) == 0)
{
auto m = b;
if (text[i] != lo[i])
{
bool all = a.size() > 1 ? text[i + a.size() - 1] != lo[i + a.size() - 1]
: i + 1 < text.size() && text[i + 1] != lo[i + 1];
if (all)
m = upper(m);
else
m[0] = upper(m.substr(0, 1))[0];
}
out += m;
i += a.size();
found = true;
break;
}
if (!found)
out += text[i++];
}
}
return out;
}
Template parseTemplate(const std::wstring &text)
{
Template t;
size_t cursor = std::wstring::npos;
for (size_t i = 0; i < text.size();)
{
if (text.compare(i, 10, L"{{cursor}}\0", 10) == 0)
{
t.text += L"{cursor}";
i += 10;
}
else if (text.compare(i, 8, L"{cursor}") == 0)
{
cursor = t.text.size();
++t.cursors;
i += 8;
}
else
t.text += text[i++];
}
if (cursor != text.npos)
t.suffix = t.text.substr(cursor);
return t;
}
namespace
{
const std::vector<std::pair<UINT, std::wstring>> &namedKeys()
{
static const std::vector<std::pair<UINT, std::wstring>> names = {
{VK_SPACE, L"Space"}, {VK_TAB, L"Tab"},
{VK_RETURN, L"Enter"}, {VK_ESCAPE, L"Escape"},
{VK_BACK, L"Backspace"}, {VK_DELETE, L"Delete"},
{VK_INSERT, L"Insert"}, {VK_PAUSE, L"Pause"},
{VK_SCROLL, L"ScrollLock"}, {VK_HOME, L"Home"},
{VK_END, L"End"}, {VK_PRIOR, L"PageUp"},
{VK_NEXT, L"PageDown"}, {VK_LEFT, L"Left"},
{VK_RIGHT, L"Right"}, {VK_UP, L"Up"},
{VK_DOWN, L"Down"}, {VK_SNAPSHOT, L"PrintScreen"},
{VK_OEM_1, L"Semicolon"}, {VK_OEM_2, L"Slash"},
{VK_OEM_3, L"Backtick"}, {VK_OEM_4, L"LeftBracket"},
{VK_OEM_5, L"Backslash"}, {VK_OEM_6, L"RightBracket"},
{VK_OEM_7, L"Quote"}, {VK_OEM_PLUS, L"Equal"},
{VK_OEM_MINUS, L"Minus"}, {VK_OEM_COMMA, L"Comma"},
{VK_OEM_PERIOD, L"Period"}, {VK_MULTIPLY, L"Multiply"},
{VK_ADD, L"Add"}, {VK_SUBTRACT, L"Subtract"},
{VK_DIVIDE, L"Divide"}, {VK_DECIMAL, L"Decimal"}};
return names;
}
} // namespace
std::wstring Hotkey::format() const
{
if (!key)
return {};
std::wstring result;
if (modifiers & MOD_CONTROL)
result += L"Ctrl+";
if (modifiers & MOD_ALT)
result += L"Alt+";
if (modifiers & MOD_SHIFT)
result += L"Shift+";
if (modifiers & MOD_WIN)
result += L"Win+";
if ((key >= L'A' && key <= L'Z') || (key >= L'0' && key <= L'9'))
return result + wchar_t(key);
if (key >= VK_F1 && key <= VK_F24)
return result + L"F" + std::to_wstring(key - VK_F1 + 1);
if (key >= VK_NUMPAD0 && key <= VK_NUMPAD9)
return result + L"Num" + std::to_wstring(key - VK_NUMPAD0);
for (auto &[vk, name] : namedKeys())
if (key == vk)
return result + name;
return {};
}
std::optional<Hotkey> Hotkey::parse(std::wstring s)
{
Hotkey h;
if (s.empty())
return h;
s = lower(s);
size_t pos = 0;
while (true)
{
size_t end = s.find(L'+', pos);
auto token = s.substr(pos, end == s.npos ? s.npos : end - pos);
if (token == L"ctrl")
h.modifiers |= MOD_CONTROL;
else if (token == L"alt")
h.modifiers |= MOD_ALT;
else if (token == L"shift")
h.modifiers |= MOD_SHIFT;
else if (token == L"win")
h.modifiers |= MOD_WIN;
else
{
if (h.key)
return {};
if (token.size() == 1 && ((token[0] >= L'a' && token[0] <= L'z') || iswdigit(token[0])))
h.key = upper(token)[0];
else if (token.size() == 4 && token.substr(0, 3) == L"num" && token[3] >= L'0' &&
token[3] <= L'9')
h.key = VK_NUMPAD0 + token[3] - L'0';
else if (token.size() >= 2 && token[0] == L'f')
{
wchar_t *tail{};
long n = wcstol(token.c_str() + 1, &tail, 10);
if (*tail || n < 1 || n > 24)
return {};
h.key = VK_F1 + (UINT)n - 1;
}
else
{
for (auto &[key, name] : namedKeys())
if (lower(name) == token)
{
h.key = key;
break;
}
if (!h.key)
return {};
}
}
if (end == s.npos)
break;
pos = end + 1;
}
return h.key ? std::optional<Hotkey>(h) : std::nullopt;
}
std::string utf8(const std::wstring &s)
{
if (s.empty())
return {};
int n = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, s.data(), (int)s.size(), nullptr, 0, nullptr,
nullptr);
if (!n)
throw std::runtime_error("Invalid Unicode");
std::string out(n, 0);
WideCharToMultiByte(CP_UTF8, 0, s.data(), (int)s.size(), out.data(), n, nullptr, nullptr);
return out;
}
std::wstring wide(const std::string &s)
{
if (s.empty())
return {};
int n = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, s.data(), (int)s.size(), nullptr, 0);
if (!n)
throw std::runtime_error("Invalid UTF-8");
std::wstring out(n, 0);
MultiByteToWideChar(CP_UTF8, 0, s.data(), (int)s.size(), out.data(), n);
return out;
}
void Run::reset()
{
keys.clear();
window = nullptr;
layout = nullptr;
overflow = false;
}
void Run::record(wchar_t c, HWND w, HKL l, ULONGLONG now)
{
if (!w || c < 32)
{
reset();
return;
}
if (window != w || layout != l || now - at >= 2000 ||
(!keys.empty() && iswspace(keys.back()) && !iswspace(c)))
reset();
window = w;
layout = l;
at = now;
if (overflow)
{
if (iswspace(c))
reset();
return;
}
if (keys.empty() && iswspace(c))
return;
if (keys.size() >= 512)
{
keys.clear();
overflow = true;
return;
}
keys += c;
}
void Run::backspace(HWND w, ULONGLONG now)
{
if (w != window || now - at >= 2000 || overflow)
reset();
else if (!keys.empty())
keys.pop_back();
at = now;
}
int Run::match(const std::wstring &text, const std::vector<Layout> &layouts, const Layout &physical) const
{
if (text.size() != keys.size() || overflow)
return -1;
for (int i = 0; i < (int)layouts.size(); ++i)
if (layouts[i].convertible)
{
std::wstring expected;
for (wchar_t c : keys)
expected += c == L' ' ? L' ' : layouts[i].at(physical.code(c));
if (lower(expected) == lower(text))
return i;
}
return -1;
}
} // namespace ks