478 lines
15 KiB
C++
478 lines
15 KiB
C++
#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
|