Files
hrynco-kswitcher/src/core.cpp
T

478 lines
15 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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, 9, L"{{enter}}") == 0)
{
t.text += L"{enter}";
i += 9;
}
else if (text.compare(i, 7, L"{enter}") == 0)
{
++t.enters;
i += 7;
}
else 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