add auto attaching and forgetting processes. make settings text consistent. other refactors

This commit is contained in:
Akash Mozumdar 2019-06-27 12:39:44 +05:30
parent 50af685d96
commit f87da8aedf
6 changed files with 102 additions and 55 deletions

View File

@ -281,13 +281,14 @@ namespace Util
return {}; return {};
} }
std::vector<DWORD> GetAllProcessIds() std::vector<std::pair<DWORD, std::optional<std::wstring>>> GetAllProcesses()
{ {
std::vector<DWORD> processIds(10000); std::vector<DWORD> processIds(10000);
DWORD spaceUsed = 0; DWORD spaceUsed = 0;
EnumProcesses(processIds.data(), 10000 * sizeof(DWORD), &spaceUsed); EnumProcesses(processIds.data(), 10000 * sizeof(DWORD), &spaceUsed);
processIds.resize(spaceUsed / sizeof(DWORD)); std::vector<std::pair<DWORD, std::optional<std::wstring>>> processes;
return processIds; for (int i = 0; i < spaceUsed / sizeof(DWORD); ++i) processes.push_back({ processIds[i], Util::GetModuleFilename(processIds[i]) });
return processes;
} }
std::optional<std::wstring> GetClipboardText() std::optional<std::wstring> GetClipboardText()

View File

@ -7,7 +7,7 @@ namespace Util
{ {
std::optional<std::wstring> GetModuleFilename(DWORD processId, HMODULE module = NULL); std::optional<std::wstring> GetModuleFilename(DWORD processId, HMODULE module = NULL);
std::optional<std::wstring> GetModuleFilename(HMODULE module = NULL); std::optional<std::wstring> GetModuleFilename(HMODULE module = NULL);
std::vector<DWORD> GetAllProcessIds(); std::vector<std::pair<DWORD, std::optional<std::wstring>>> GetAllProcesses();
std::optional<std::wstring> GetClipboardText(); std::optional<std::wstring> GetClipboardText();
std::optional<std::wstring> StringToWideString(const std::string& text, UINT encoding = CP_UTF8); std::optional<std::wstring> StringToWideString(const std::string& text, UINT encoding = CP_UTF8);
// return true if repetition found (see https://github.com/Artikash/Textractor/issues/40) // return true if repetition found (see https://github.com/Artikash/Textractor/issues/40)

View File

@ -1,10 +1,30 @@
#include "mainwindow.h" #include "mainwindow.h"
#include "host/util.h" #include "host/util.h"
#include <winhttp.h>
#include <QApplication> #include <QApplication>
extern const wchar_t* UPDATE_AVAILABLE;
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
std::thread([]
{
using InternetHandle = AutoHandle<Functor<WinHttpCloseHandle>>;
// Queries GitHub releases API https://developer.github.com/v3/repos/releases/ and checks the last release tag to check if it's the same
if (InternetHandle internet = WinHttpOpen(L"Mozilla/5.0 Textractor", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, NULL, NULL, 0))
if (InternetHandle connection = WinHttpConnect(internet, L"api.github.com", INTERNET_DEFAULT_HTTPS_PORT, 0))
if (InternetHandle request = WinHttpOpenRequest(connection, L"GET", L"/repos/Artikash/Textractor/releases", NULL, NULL, NULL, WINHTTP_FLAG_SECURE))
if (WinHttpSendRequest(request, NULL, 0, NULL, 0, 0, NULL))
{
char buffer[1000] = {};
WinHttpReceiveResponse(request, NULL);
WinHttpReadData(request, buffer, 1000, DUMMY);
if (abs(strstr(buffer, "/tag/") - strstr(buffer, VERSION)) > 10) MESSAGE(UPDATE_AVAILABLE);
}
}).detach();
QDir::setCurrent(QFileInfo(S(Util::GetModuleFilename().value())).absolutePath()); QDir::setCurrent(QFileInfo(S(Util::GetModuleFilename().value())).absolutePath());
return QApplication(argc, argv), MainWindow().show(), QApplication::exec(); QApplication app(argc, argv);
return MainWindow().show(), app.exec();
} }

View File

@ -1,10 +1,8 @@
#include "mainwindow.h" #include "mainwindow.h"
#include "ui_mainwindow.h" #include "ui_mainwindow.h"
#include "defs.h" #include "defs.h"
#include "extenwindow.h"
#include "host/util.h" #include "host/util.h"
#include <shellapi.h> #include <shellapi.h>
#include <winhttp.h>
#include <QFormLayout> #include <QFormLayout>
#include <QLabel> #include <QLabel>
#include <QPushButton> #include <QPushButton>
@ -19,6 +17,7 @@
extern const char* ATTACH; extern const char* ATTACH;
extern const char* LAUNCH; extern const char* LAUNCH;
extern const char* DETACH; extern const char* DETACH;
extern const char* FORGET;
extern const char* ADD_HOOK; extern const char* ADD_HOOK;
extern const char* REMOVE_HOOKS; extern const char* REMOVE_HOOKS;
extern const char* SAVE_HOOKS; extern const char* SAVE_HOOKS;
@ -46,12 +45,13 @@ extern const char* DOUBLE_CLICK_TO_REMOVE_HOOK;
extern const char* SAVE_SETTINGS; extern const char* SAVE_SETTINGS;
extern const char* USE_JP_LOCALE; extern const char* USE_JP_LOCALE;
extern const char* FILTER_REPETITION; extern const char* FILTER_REPETITION;
extern const char* AUTO_ATTACH;
extern const char* ATTACH_SAVED_ONLY;
extern const char* DEFAULT_CODEPAGE; extern const char* DEFAULT_CODEPAGE;
extern const char* FLUSH_DELAY; extern const char* FLUSH_DELAY;
extern const char* MAX_BUFFER_SIZE; extern const char* MAX_BUFFER_SIZE;
extern const wchar_t* ABOUT; extern const wchar_t* ABOUT;
extern const wchar_t* CL_OPTIONS; extern const wchar_t* CL_OPTIONS;
extern const wchar_t* UPDATE_AVAILABLE;
extern const wchar_t* LAUNCH_FAILED; extern const wchar_t* LAUNCH_FAILED;
extern const wchar_t* INVALID_CODE; extern const wchar_t* INVALID_CODE;
@ -65,6 +65,7 @@ MainWindow::MainWindow(QWidget *parent) :
{ ATTACH, &MainWindow::AttachProcess }, { ATTACH, &MainWindow::AttachProcess },
{ LAUNCH, &MainWindow::LaunchProcess }, { LAUNCH, &MainWindow::LaunchProcess },
{ DETACH, &MainWindow::DetachProcess }, { DETACH, &MainWindow::DetachProcess },
{ FORGET, &MainWindow::ForgetProcess },
{ ADD_HOOK, &MainWindow::AddHook }, { ADD_HOOK, &MainWindow::AddHook },
{ REMOVE_HOOKS, &MainWindow::RemoveHooks }, { REMOVE_HOOKS, &MainWindow::RemoveHooks },
{ SAVE_HOOKS, &MainWindow::SaveHooks }, { SAVE_HOOKS, &MainWindow::SaveHooks },
@ -86,6 +87,8 @@ MainWindow::MainWindow(QWidget *parent) :
QSettings settings(CONFIG_FILE, QSettings::IniFormat); QSettings settings(CONFIG_FILE, QSettings::IniFormat);
if (settings.contains(WINDOW)) setGeometry(settings.value(WINDOW).toRect()); if (settings.contains(WINDOW)) setGeometry(settings.value(WINDOW).toRect());
TextThread::filterRepetition = settings.value(FILTER_REPETITION, TextThread::filterRepetition).toBool(); TextThread::filterRepetition = settings.value(FILTER_REPETITION, TextThread::filterRepetition).toBool();
autoAttach = settings.value(AUTO_ATTACH, autoAttach).toBool();
autoAttachSavedOnly = settings.value(ATTACH_SAVED_ONLY, autoAttachSavedOnly).toBool();
TextThread::flushDelay = settings.value(FLUSH_DELAY, TextThread::flushDelay).toInt(); TextThread::flushDelay = settings.value(FLUSH_DELAY, TextThread::flushDelay).toInt();
TextThread::maxBufferSize = settings.value(MAX_BUFFER_SIZE, TextThread::maxBufferSize).toInt(); TextThread::maxBufferSize = settings.value(MAX_BUFFER_SIZE, TextThread::maxBufferSize).toInt();
Host::defaultCodepage = settings.value(DEFAULT_CODEPAGE, Host::defaultCodepage).toInt(); Host::defaultCodepage = settings.value(DEFAULT_CODEPAGE, Host::defaultCodepage).toInt();
@ -102,32 +105,32 @@ MainWindow::MainWindow(QWidget *parent) :
AttachConsole(ATTACH_PARENT_PROCESS); AttachConsole(ATTACH_PARENT_PROCESS);
WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE), CL_OPTIONS, wcslen(CL_OPTIONS), DUMMY, NULL); WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE), CL_OPTIONS, wcslen(CL_OPTIONS), DUMMY, NULL);
std::vector<DWORD> processIds = Util::GetAllProcessIds(); auto processes = Util::GetAllProcesses();
std::vector<std::wstring> processNames;
for (auto processId : processIds) processNames.emplace_back(Util::GetModuleFilename(processId).value_or(L""));
int argc; int argc;
std::unique_ptr<LPWSTR[], Functor<LocalFree>> argv(CommandLineToArgvW(GetCommandLineW(), &argc)); std::unique_ptr<LPWSTR[], Functor<LocalFree>> argv(CommandLineToArgvW(GetCommandLineW(), &argc));
for (int i = 0; i < argc; ++i) for (int i = 0; i < argc; ++i)
if (std::wstring arg = argv[i]; arg[0] == L'/' || arg[0] == L'-') if (std::wstring arg = argv[i]; arg[0] == L'/' || arg[0] == L'-')
if (arg[1] == L'p' || arg[1] == L'P') if (arg[1] == L'p' || arg[1] == L'P')
if (DWORD processId = _wtoi(arg.substr(2).c_str())) Host::InjectProcess(processId); if (DWORD processId = _wtoi(arg.substr(2).c_str())) Host::InjectProcess(processId);
else for (int i = 0; i < processIds.size(); ++i) else for (auto [processId, processName] : processes)
if (processNames[i].find(L"\\" + arg.substr(2)) != std::wstring::npos) Host::InjectProcess(processIds[i]); if (processName.value_or(L"").find(L"\\" + arg.substr(2)) != std::wstring::npos) Host::InjectProcess(processId);
std::thread([] std::thread([this]
{ {
using InternetHandle = AutoHandle<Functor<WinHttpCloseHandle>>; for (; ; Sleep(10000))
// Queries GitHub releases API https://developer.github.com/v3/repos/releases/ and checks the last release tag to check if it's the same {
if (InternetHandle internet = WinHttpOpen(L"Mozilla/5.0 Textractor", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, NULL, NULL, 0)) std::unordered_set<std::wstring> attachTargets;
if (InternetHandle connection = WinHttpConnect(internet, L"api.github.com", INTERNET_DEFAULT_HTTPS_PORT, 0)) if (autoAttach)
if (InternetHandle request = WinHttpOpenRequest(connection, L"GET", L"/repos/Artikash/Textractor/releases", NULL, NULL, NULL, WINHTTP_FLAG_SECURE)) for (auto process : QString(QTextFile(GAME_SAVE_FILE, QIODevice::ReadOnly).readAll()).split("\n", QString::SkipEmptyParts))
if (WinHttpSendRequest(request, NULL, 0, NULL, 0, 0, NULL)) attachTargets.insert(S(process));
{ if (autoAttachSavedOnly)
char buffer[1000] = {}; for (auto process : QString(QTextFile(HOOK_SAVE_FILE, QIODevice::ReadOnly).readAll()).split("\n", QString::SkipEmptyParts))
WinHttpReceiveResponse(request, NULL); attachTargets.insert(S(process.split(" , ")[0]));
WinHttpReadData(request, buffer, 1000, DUMMY);
if (abs(strstr(buffer, "/tag/") - strstr(buffer, VERSION)) > 10) MESSAGE(UPDATE_AVAILABLE); if (!attachTargets.empty())
} for (auto [processId, processName] : Util::GetAllProcesses())
if (processName && attachTargets.count(processName.value()) > 0 && alreadyAttached.count(processId) == 0) Host::InjectProcess(processId);
}
}).detach(); }).detach();
} }
@ -146,7 +149,8 @@ void MainWindow::closeEvent(QCloseEvent*)
void MainWindow::ProcessConnected(DWORD processId) void MainWindow::ProcessConnected(DWORD processId)
{ {
if (processId == 0) return; alreadyAttached.insert(processId);
QString process = S(Util::GetModuleFilename(processId).value_or(L"???")); QString process = S(Util::GetModuleFilename(processId).value_or(L"???"));
QMetaObject::invokeMethod(this, [this, process, processId] QMetaObject::invokeMethod(this, [this, process, processId]
{ {
@ -163,7 +167,7 @@ void MainWindow::ProcessConnected(DWORD processId)
if (hookList != allProcesses.rend()) if (hookList != allProcesses.rend())
for (auto hookInfo : hookList->split(" , ")) for (auto hookInfo : hookList->split(" , "))
if (auto hp = Util::ParseCode(S(hookInfo))) Host::InsertHook(processId, hp.value()); if (auto hp = Util::ParseCode(S(hookInfo))) Host::InsertHook(processId, hp.value());
else swscanf_s(S(hookInfo).c_str(), L"|%I64d:%I64d:%[^\n]", &savedThreadCtx.first, &savedThreadCtx.second, savedThreadCode, (unsigned)std::size(savedThreadCode)); else swscanf_s(S(hookInfo).c_str(), L"|%I64d:%I64d:%[^\n]", &savedThreadCtx, &savedThreadCtx2, savedThreadCode, (unsigned)std::size(savedThreadCode));
} }
void MainWindow::ProcessDisconnected(DWORD processId) void MainWindow::ProcessDisconnected(DWORD processId)
@ -178,8 +182,8 @@ void MainWindow::ThreadAdded(TextThread& thread)
{ {
std::wstring threadCode = Util::GenerateCode(thread.hp, thread.tp.processId); std::wstring threadCode = Util::GenerateCode(thread.hp, thread.tp.processId);
QString ttString = TextThreadString(thread) + S(thread.name) + " (" + S(threadCode) + ")"; QString ttString = TextThreadString(thread) + S(thread.name) + " (" + S(threadCode) + ")";
bool savedMatch = savedThreadCtx == std::pair(thread.tp.ctx, thread.tp.ctx2) && savedThreadCode == threadCode; bool savedMatch = savedThreadCtx == thread.tp.ctx && savedThreadCtx2 == thread.tp.ctx2 && savedThreadCode == threadCode;
if (savedMatch) savedThreadCtx.first = savedThreadCtx.second = savedThreadCode[0] = 0; if (savedMatch) savedThreadCtx = savedThreadCtx2 = savedThreadCode[0] = 0;
QMetaObject::invokeMethod(this, [this, ttString, savedMatch] QMetaObject::invokeMethod(this, [this, ttString, savedMatch]
{ {
ui->ttCombo->addItem(ttString); ui->ttCombo->addItem(ttString);
@ -259,8 +263,8 @@ std::array<InfoForExtension, 10> MainWindow::GetMiscInfo(TextThread& thread)
void MainWindow::AttachProcess() void MainWindow::AttachProcess()
{ {
QMultiHash<QString, DWORD> allProcesses; QMultiHash<QString, DWORD> allProcesses;
for (auto processId : Util::GetAllProcessIds()) for (auto [processId, processName] : Util::GetAllProcesses())
if (auto processName = Util::GetModuleFilename(processId)) allProcesses.insert(QFileInfo(S(processName.value())).fileName(), processId); if (processName) allProcesses.insert(QFileInfo(S(processName.value())).fileName(), processId);
QStringList processList(allProcesses.uniqueKeys()); QStringList processList(allProcesses.uniqueKeys());
processList.sort(Qt::CaseInsensitive); processList.sort(Qt::CaseInsensitive);
@ -319,6 +323,20 @@ void MainWindow::DetachProcess()
try { Host::DetachProcess(GetSelectedProcessId()); } catch (std::out_of_range) {} try { Host::DetachProcess(GetSelectedProcessId()); } catch (std::out_of_range) {}
} }
void MainWindow::ForgetProcess()
{
if (auto processName = Util::GetModuleFilename(GetSelectedProcessId()))
{
for (auto file : { GAME_SAVE_FILE, HOOK_SAVE_FILE })
{
QStringList lines = QString::fromUtf8(QTextFile(file, QIODevice::ReadOnly).readAll()).split("\n", QString::SkipEmptyParts);
lines.erase(std::remove_if(lines.begin(), lines.end(), [&](auto line) { return line.contains(S(processName.value())); }), lines.end());
QTextFile(file, QIODevice::WriteOnly | QIODevice::Truncate).write(lines.join("\n").toUtf8());
}
}
DetachProcess();
}
void MainWindow::AddHook() void MainWindow::AddHook()
{ {
if (QString hookCode = QInputDialog::getText(this, ADD_HOOK, CODE_INFODUMP, QLineEdit::Normal, "", &ok, Qt::WindowCloseButtonHint); ok) if (QString hookCode = QInputDialog::getText(this, ADD_HOOK, CODE_INFODUMP, QLineEdit::Normal, "", &ok, Qt::WindowCloseButtonHint); ok)
@ -369,8 +387,8 @@ void MainWindow::SaveHooks()
} }
} }
auto hookInfo = QStringList() << S(processName.value()) << hookCodes.values(); auto hookInfo = QStringList() << S(processName.value()) << hookCodes.values();
ThreadParam tp = current.load()->tp; ThreadParam tp = current->tp;
if (tp.processId == GetSelectedProcessId()) hookInfo << QString("|%1:%2:%3").arg(tp.ctx).arg(tp.ctx2).arg(S(Util::GenerateCode(Host::GetThread(tp).hp, tp.processId))); if (tp.processId == GetSelectedProcessId()) hookInfo << QString("|%1:%2:%3").arg(tp.ctx).arg(tp.ctx2).arg(S(Util::GenerateCode(current->hp, tp.processId)));
QTextFile(HOOK_SAVE_FILE, QIODevice::WriteOnly | QIODevice::Append).write((hookInfo.join(" , ") + "\n").toUtf8()); QTextFile(HOOK_SAVE_FILE, QIODevice::WriteOnly | QIODevice::Append).write((hookInfo.join(" , ") + "\n").toUtf8());
} }
@ -436,7 +454,7 @@ void MainWindow::FindHooks()
if (!dialog.exec()) return; if (!dialog.exec()) return;
QByteArray pattern = QByteArray::fromHex(patternInput.text().replace("??", QString::number(XX, 16)).toUtf8()); QByteArray pattern = QByteArray::fromHex(patternInput.text().replace("??", QString::number(XX, 16)).toUtf8());
memcpy(sp.pattern, pattern.data(), sp.length = min(pattern.size(), 25)); memcpy(sp.pattern, pattern.data(), sp.length = min(pattern.size(), 25));
try { filter = std::wregex(S(filterInput.text())); } catch (std::regex_error) {}; try { filter = std::wregex(S(filterInput.text())); } catch (std::regex_error) {}
} }
else else
{ {
@ -469,28 +487,30 @@ void MainWindow::Settings()
QSettings settings(CONFIG_FILE, QSettings::IniFormat, &dialog); QSettings settings(CONFIG_FILE, QSettings::IniFormat, &dialog);
QFormLayout layout(&dialog); QFormLayout layout(&dialog);
QPushButton saveButton(SAVE_SETTINGS, &dialog); QPushButton saveButton(SAVE_SETTINGS, &dialog);
layout.addWidget(&saveButton); for (auto [value, label] : Array<std::tuple<bool&, const char*>>{
{ TextThread::filterRepetition, FILTER_REPETITION },
{ autoAttach, AUTO_ATTACH },
{ autoAttachSavedOnly, ATTACH_SAVED_ONLY },
})
{
auto checkBox = new QCheckBox(&dialog);
checkBox->setChecked(value);
layout.addRow(label, checkBox);
connect(&saveButton, &QPushButton::clicked, [checkBox, label, &settings, &value] { settings.setValue(label, value = checkBox->isChecked()); });
}
for (auto [value, label] : Array<std::tuple<int&, const char*>>{ for (auto [value, label] : Array<std::tuple<int&, const char*>>{
{ Host::defaultCodepage, DEFAULT_CODEPAGE },
{ TextThread::maxBufferSize, MAX_BUFFER_SIZE }, { TextThread::maxBufferSize, MAX_BUFFER_SIZE },
{ TextThread::flushDelay, FLUSH_DELAY }, { TextThread::flushDelay, FLUSH_DELAY },
{ Host::defaultCodepage, DEFAULT_CODEPAGE },
}) })
{ {
auto spinBox = new QSpinBox(&dialog); auto spinBox = new QSpinBox(&dialog);
spinBox->setMaximum(INT_MAX); spinBox->setMaximum(INT_MAX);
spinBox->setValue(value); spinBox->setValue(value);
layout.insertRow(0, label, spinBox); layout.addRow(label, spinBox);
connect(&saveButton, &QPushButton::clicked, [spinBox, label, &settings, &value] { settings.setValue(label, value = spinBox->value()); }); connect(&saveButton, &QPushButton::clicked, [spinBox, label, &settings, &value] { settings.setValue(label, value = spinBox->value()); });
} }
for (auto [value, label] : Array<std::tuple<bool&, const char*>>{ layout.addWidget(&saveButton);
{ TextThread::filterRepetition, FILTER_REPETITION },
})
{
auto checkBox = new QCheckBox(&dialog);
checkBox->setChecked(value);
layout.insertRow(0, label, checkBox);
connect(&saveButton, &QPushButton::clicked, [checkBox, label, &settings, &value] { settings.setValue(label, value = checkBox->isChecked()); });
}
connect(&saveButton, &QPushButton::clicked, &dialog, &QDialog::accept); connect(&saveButton, &QPushButton::clicked, &dialog, &QDialog::accept);
dialog.setWindowTitle(SETTINGS); dialog.setWindowTitle(SETTINGS);
dialog.exec(); dialog.exec();

View File

@ -35,6 +35,7 @@ private:
void AttachProcess(); void AttachProcess();
void LaunchProcess(); void LaunchProcess();
void DetachProcess(); void DetachProcess();
void ForgetProcess();
void AddHook(); void AddHook();
void RemoveHooks(); void RemoveHooks();
void SaveHooks(); void SaveHooks();
@ -44,8 +45,10 @@ private:
void ViewThread(int index); void ViewThread(int index);
Ui::MainWindow* ui; Ui::MainWindow* ui;
QWidget* extenWindow; ExtenWindow* extenWindow;
std::pair<uint64_t, uint64_t> savedThreadCtx; std::unordered_set<DWORD> alreadyAttached;
wchar_t savedThreadCode[1000]; bool autoAttach = false, autoAttachSavedOnly = true;
std::atomic<TextThread*> current; uint64_t savedThreadCtx = 0, savedThreadCtx2 = 0;
wchar_t savedThreadCode[1000] = {};
TextThread* current = nullptr;
}; };

View File

@ -9,6 +9,7 @@
const char* ATTACH = u8"Attach to game"; const char* ATTACH = u8"Attach to game";
const char* LAUNCH = u8"Launch game"; const char* LAUNCH = u8"Launch game";
const char* DETACH = u8"Detach from game"; const char* DETACH = u8"Detach from game";
const char* FORGET = u8"Forget game";
const char* ADD_HOOK = u8"Add hook"; const char* ADD_HOOK = u8"Add hook";
const char* REMOVE_HOOKS = u8"Remove hook(s)"; const char* REMOVE_HOOKS = u8"Remove hook(s)";
const char* SAVE_HOOKS = u8"Save hook(s)"; const char* SAVE_HOOKS = u8"Save hook(s)";
@ -63,10 +64,12 @@ const char* START_HOOK_SEARCH = u8"Start hook search";
const char* SAVE_SEARCH_RESULTS = u8"Save search results"; const char* SAVE_SEARCH_RESULTS = u8"Save search results";
const char* TEXT_FILES = u8"Text (*.txt)"; const char* TEXT_FILES = u8"Text (*.txt)";
const char* DOUBLE_CLICK_TO_REMOVE_HOOK = u8"Double click a hook to remove it"; const char* DOUBLE_CLICK_TO_REMOVE_HOOK = u8"Double click a hook to remove it";
const char* FILTER_REPETITION = u8"Repetition Filter"; const char* FILTER_REPETITION = u8"Filter repetition";
const char* DEFAULT_CODEPAGE = u8"Default Codepage"; const char* AUTO_ATTACH = u8"Auto attach";
const char* FLUSH_DELAY = u8"Flush Delay"; const char* ATTACH_SAVED_ONLY = u8"Auto attach (saved only)";
const char* MAX_BUFFER_SIZE = u8"Max Buffer Size"; const char* DEFAULT_CODEPAGE = u8"Default codepage";
const char* FLUSH_DELAY = u8"Flush delay";
const char* MAX_BUFFER_SIZE = u8"Max buffer size";
const wchar_t* CONSOLE = L"Console"; const wchar_t* CONSOLE = L"Console";
const wchar_t* CLIPBOARD = L"Clipboard"; const wchar_t* CLIPBOARD = L"Clipboard";
const wchar_t* ABOUT = L"Textractor " ARCH L" v" VERSION LR"( made by me: Artikash (email: akashmozumdar@gmail.com) const wchar_t* ABOUT = L"Textractor " ARCH L" v" VERSION LR"( made by me: Artikash (email: akashmozumdar@gmail.com)