Theme preview/revert, theme editor enhancements, build and deploy updates

This commit is contained in:
IChooseYou
2026-02-12 12:37:09 -07:00
committed by sysadmin
parent e73b783cda
commit 4b1d3e9d3f
18 changed files with 548 additions and 120 deletions

View File

@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.20) cmake_minimum_required(VERSION 3.20)
project(ReclassX VERSION 0.1 LANGUAGES CXX) project(Reclass VERSION 0.1 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -30,7 +30,7 @@ endif()
find_package(QScintilla REQUIRED) find_package(QScintilla REQUIRED)
add_executable(ReclassX add_executable(Reclass
src/main.cpp src/main.cpp
src/editor.h src/editor.h
src/editor.cpp src/editor.cpp
@@ -64,9 +64,9 @@ add_executable(ReclassX
src/mcp/mcp_bridge.cpp src/mcp/mcp_bridge.cpp
) )
target_include_directories(ReclassX PRIVATE src) target_include_directories(Reclass PRIVATE src)
target_link_libraries(ReclassX PRIVATE target_link_libraries(Reclass PRIVATE
${QT}::Widgets ${QT}::Widgets
${QT}::PrintSupport ${QT}::PrintSupport
${QT}::Svg ${QT}::Svg
@@ -76,7 +76,7 @@ target_link_libraries(ReclassX PRIVATE
${_QT_WINEXTRAS} ${_QT_WINEXTRAS}
) )
if(WIN32) if(WIN32)
target_link_libraries(ReclassX PRIVATE dbghelp dwmapi psapi) target_link_libraries(Reclass PRIVATE dbghelp dwmapi psapi)
endif() endif()
add_executable(rcx-mcp-stdio tools/rcx-mcp-stdio.cpp) add_executable(rcx-mcp-stdio tools/rcx-mcp-stdio.cpp)
@@ -85,8 +85,8 @@ target_link_libraries(rcx-mcp-stdio PRIVATE ${QT}::Core ${QT}::Network)
include(deploy) include(deploy)
add_custom_target(screenshot ALL add_custom_target(screenshot ALL
COMMAND ReclassX --screenshot ${CMAKE_BINARY_DIR}/screenshot.png COMMAND Reclass --screenshot ${CMAKE_BINARY_DIR}/screenshot.png
DEPENDS ReclassX deploy DEPENDS Reclass deploy
WORKING_DIRECTORY ${CMAKE_BINARY_DIR} WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
COMMENT "Capturing UI screenshot with class open..." COMMENT "Capturing UI screenshot with class open..."
) )
@@ -114,7 +114,7 @@ message(STATUS \"Combined sources -> \${_out}\")
add_custom_target(combined ALL add_custom_target(combined ALL
COMMAND ${CMAKE_COMMAND} -P ${_combine_script} COMMAND ${CMAKE_COMMAND} -P ${_combine_script}
DEPENDS ReclassX DEPENDS Reclass
COMMENT "Combining all source files into h_cpp_combined.txt" COMMENT "Combining all source files into h_cpp_combined.txt"
) )

View File

@@ -20,8 +20,8 @@ This tool helps you inspect raw bytes and interpret them as types (structs, arra
2. Quick Build (relies on powershell| for manual build skip to step 3) 2. Quick Build (relies on powershell| for manual build skip to step 3)
git clone --recurse-submodules https://github.com/IChooseYou/ReclassX.git git clone --recurse-submodules https://github.com/IChooseYou/Reclass.git
cd ReclassX cd Reclass
.\scripts\build_qscintilla.ps1 .\scripts\build_qscintilla.ps1
.\scripts\build.ps1 .\scripts\build.ps1
^ script above tries to autodetect Qt install (as we learned not everyone installs to C:/Qt/) ^ script above tries to autodetect Qt install (as we learned not everyone installs to C:/Qt/)

View File

@@ -69,14 +69,14 @@ endif()
if(TARGET ${QT}::windeployqt) if(TARGET ${QT}::windeployqt)
add_custom_target(deploy add_custom_target(deploy
COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_LIST_DIR}/deploy.cmake COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_LIST_DIR}/deploy.cmake
$<TARGET_FILE:ReclassX> $<TARGET_FILE:Reclass>
$<TARGET_FILE:${QT}::windeployqt> $<TARGET_FILE:${QT}::windeployqt>
DEPENDS ReclassX DEPENDS Reclass
COMMENT "Deploying Qt runtime DLLs..." COMMENT "Deploying Qt runtime DLLs..."
) )
# Force re-deploy on rebuild # Force re-deploy on rebuild
set_target_properties(deploy PROPERTIES set_target_properties(deploy PROPERTIES
ADDITIONAL_CLEAN_FILES $<TARGET_FILE_DIR:ReclassX>/.qt_deployed ADDITIONAL_CLEAN_FILES $<TARGET_FILE_DIR:Reclass>/.qt_deployed
) )
endif() endif()

View File

@@ -64,7 +64,7 @@ class ProcessMemoryPlugin : public IProviderPlugin
public: public:
std::string Name() const override { return "Process Memory"; } std::string Name() const override { return "Process Memory"; }
std::string Version() const override { return "1.0.0"; } std::string Version() const override { return "1.0.0"; }
std::string Author() const override { return "ReclassX"; } std::string Author() const override { return "Reclass"; }
std::string Description() const override { return "Read and write memory from local running processes"; } std::string Description() const override { return "Read and write memory from local running processes"; }
k_ELoadType LoadType() const override { return k_ELoadTypeAuto; } k_ELoadType LoadType() const override { return k_ELoadTypeAuto; }
QIcon Icon() const override; QIcon Icon() const override;

View File

@@ -1,4 +1,4 @@
# PowerShell script to build ReclassX # PowerShell script to build Reclass
# Automatically detects Qt installation and configures build environment # Automatically detects Qt installation and configures build environment
#Requires -Version 5.1 #Requires -Version 5.1
@@ -303,7 +303,7 @@ function Find-MinGWDirectory {
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────
Write-ColorOutput "`n========================================" Cyan Write-ColorOutput "`n========================================" Cyan
Write-ColorOutput "ReclassX Build Script" Cyan Write-ColorOutput "Reclass Build Script" Cyan
Write-ColorOutput "========================================`n" Cyan Write-ColorOutput "========================================`n" Cyan
# Get script directory and project root # Get script directory and project root
@@ -426,7 +426,7 @@ try {
Write-ColorOutput "`nCMake configuration completed successfully.`n" Green Write-ColorOutput "`nCMake configuration completed successfully.`n" Green
# Build # Build
Write-ColorOutput "Building ReclassX..." Cyan Write-ColorOutput "Building Reclass..." Cyan
$cores = (Get-CimInstance -ClassName Win32_Processor).NumberOfLogicalProcessors $cores = (Get-CimInstance -ClassName Win32_Processor).NumberOfLogicalProcessors
if (-not $cores -or $cores -lt 1) { if (-not $cores -or $cores -lt 1) {
@@ -445,8 +445,8 @@ try {
# Find executable # Find executable
Write-ColorOutput "Locating executable..." Cyan Write-ColorOutput "Locating executable..." Cyan
$exePaths = @( $exePaths = @(
(Join-Path $buildDir "ReclassX.exe"), (Join-Path $buildDir "Reclass.exe"),
(Join-Path $buildDir "$BuildType\ReclassX.exe") (Join-Path $buildDir "$BuildType\Reclass.exe")
) )
$exePath = $null $exePath = $null
@@ -477,7 +477,7 @@ try {
# Count deployed files # Count deployed files
$deployedFiles = Get-ChildItem -Path $exeDir -Recurse -File | Where-Object { $deployedFiles = Get-ChildItem -Path $exeDir -Recurse -File | Where-Object {
$_.Name -ne "ReclassX.exe" -and $_.Extension -match '\.(dll|qm)$' $_.Name -ne "Reclass.exe" -and $_.Extension -match '\.(dll|qm)$'
} }
if ($deployedFiles) { if ($deployedFiles) {
Write-ColorOutput "Deployed $($deployedFiles.Count) Qt dependency files." Gray Write-ColorOutput "Deployed $($deployedFiles.Count) Qt dependency files." Gray
@@ -491,7 +491,7 @@ try {
Write-ColorOutput "Application may not run without Qt DLLs in PATH" Yellow Write-ColorOutput "Application may not run without Qt DLLs in PATH" Yellow
} }
} else { } else {
Write-ColorOutput "WARNING: Could not locate ReclassX.exe" Yellow Write-ColorOutput "WARNING: Could not locate Reclass.exe" Yellow
} }
} catch { } catch {
@@ -507,5 +507,5 @@ Write-ColorOutput "========================================`n" Cyan
if ($exePath) { if ($exePath) {
Write-ColorOutput "Run the application with:" White Write-ColorOutput "Run the application with:" White
Write-ColorOutput " .\build\ReclassX.exe`n" Cyan Write-ColorOutput " .\build\Reclass.exe`n" Cyan
} }

View File

@@ -1,4 +1,4 @@
# PowerShell script to build QScintilla static library for ReclassX # PowerShell script to build QScintilla static library for Reclass
# This script checks for Qt installation, prompts if missing, and builds QScintilla # This script checks for Qt installation, prompts if missing, and builds QScintilla
#Requires -Version 5.1 #Requires -Version 5.1
@@ -272,7 +272,7 @@ function Find-MakeCommand {
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────
Write-ColorOutput "`n========================================" Cyan Write-ColorOutput "`n========================================" Cyan
Write-ColorOutput "QScintilla Build Script for ReclassX" Cyan Write-ColorOutput "QScintilla Build Script for Reclass" Cyan
Write-ColorOutput "========================================`n" Cyan Write-ColorOutput "========================================`n" Cyan
# Get script directory and project root # Get script directory and project root
@@ -423,7 +423,7 @@ try {
Write-Host " - $($lib.Name) ($sizeMB MB)" -ForegroundColor Green Write-Host " - $($lib.Name) ($sizeMB MB)" -ForegroundColor Green
Write-Host " Path: $($lib.Path)" -ForegroundColor Gray Write-Host " Path: $($lib.Path)" -ForegroundColor Gray
} }
Write-ColorOutput "`nYou can now build ReclassX with CMake." Green Write-ColorOutput "`nYou can now build Reclass with CMake." Green
} else { } else {
Write-ColorOutput "`nWARNING: Build completed but no library files found." Yellow Write-ColorOutput "`nWARNING: Build completed but no library files found." Yellow
Write-ColorOutput "Expected files: qscintilla2_qt6.a or qscintilla2_qt6.lib" Yellow Write-ColorOutput "Expected files: qscintilla2_qt6.a or qscintilla2_qt6.lib" Yellow

View File

@@ -1622,7 +1622,7 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
} }
// ── Font with zoom ── // ── Font with zoom ──
QSettings settings("ReclassX", "ReclassX"); QSettings settings("Reclass", "Reclass");
QString fontName = settings.value("font", "JetBrains Mono").toString(); QString fontName = settings.value("font", "JetBrains Mono").toString();
QFont font(fontName, 12); QFont font(fontName, 12);
font.setFixedPitch(true); font.setFixedPitch(true);

View File

@@ -163,6 +163,7 @@ enum Marker : int {
M_HOVER = 6, M_HOVER = 6,
M_SELECTED = 7, M_SELECTED = 7,
M_CMD_ROW = 8, M_CMD_ROW = 8,
M_ACCENT = 9,
}; };
// ── Node ── // ── Node ──

View File

@@ -201,8 +201,11 @@ void RcxEditor::setupMargins() {
m_sci->setMarginWidth(0, " 00000000 "); // default 8-digit; resized dynamically in applyDocument() m_sci->setMarginWidth(0, " 00000000 "); // default 8-digit; resized dynamically in applyDocument()
m_sci->setMarginSensitivity(0, true); m_sci->setMarginSensitivity(0, true);
// Margin 1: hidden (fold chevrons moved to text column) // Margin 1: 2px accent bar (selection indicator)
m_sci->setMarginWidth(1, 0); m_sci->setMarginType(1, QsciScintilla::SymbolMargin);
m_sci->setMarginWidth(1, 2);
m_sci->setMarginSensitivity(1, false);
m_sci->setMarginMarkerMask(1, 1 << M_ACCENT);
} }
void RcxEditor::setupFolding() { void RcxEditor::setupFolding() {
@@ -252,6 +255,9 @@ void RcxEditor::setupMarkers() {
// M_CMD_ROW (8): distinct background for CommandRow bar // M_CMD_ROW (8): distinct background for CommandRow bar
m_sci->markerDefine(QsciScintilla::Background, M_CMD_ROW); m_sci->markerDefine(QsciScintilla::Background, M_CMD_ROW);
// M_ACCENT (9): 2px accent bar in margin 1 (selection indicator)
m_sci->markerDefine(QsciScintilla::FullRectangle, M_ACCENT);
} }
void RcxEditor::allocateMarginStyles() { void RcxEditor::allocateMarginStyles() {
@@ -329,6 +335,7 @@ void RcxEditor::applyTheme(const Theme& theme) {
m_sci->setMarkerBackgroundColor(theme.hover, M_HOVER); m_sci->setMarkerBackgroundColor(theme.hover, M_HOVER);
m_sci->setMarkerBackgroundColor(theme.selected, M_SELECTED); m_sci->setMarkerBackgroundColor(theme.selected, M_SELECTED);
m_sci->setMarkerBackgroundColor(theme.background, M_CMD_ROW); m_sci->setMarkerBackgroundColor(theme.background, M_CMD_ROW);
m_sci->setMarkerBackgroundColor(theme.indHoverSpan, M_ACCENT);
// Margin extended styles // Margin extended styles
if (m_marginStyleBase >= 0) { if (m_marginStyleBase >= 0) {
@@ -493,6 +500,7 @@ void RcxEditor::applyHexDimming(const QVector<LineMeta>& meta) {
void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) { void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) {
m_currentSelIds = selIds; m_currentSelIds = selIds;
m_sci->markerDeleteAll(M_SELECTED); m_sci->markerDeleteAll(M_SELECTED);
m_sci->markerDeleteAll(M_ACCENT);
// Clear all editable indicators, then repaint for selected lines only // Clear all editable indicators, then repaint for selected lines only
long docLen = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLENGTH); long docLen = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLENGTH);
@@ -508,6 +516,7 @@ void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) {
uint64_t checkId = isFooter ? (nodeId | kFooterIdBit) : nodeId; uint64_t checkId = isFooter ? (nodeId | kFooterIdBit) : nodeId;
if (selIds.contains(checkId)) { if (selIds.contains(checkId)) {
m_sci->markerAdd(i, M_SELECTED); m_sci->markerAdd(i, M_SELECTED);
m_sci->markerAdd(i, M_ACCENT);
if (!isFooter) if (!isFooter)
paintEditableSpans(i); paintEditableSpans(i);
} }
@@ -1477,6 +1486,9 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
} }
auto* lm = metaForLine(line); auto* lm = metaForLine(line);
if (!lm) return false; if (!lm) return false;
// Reject lines that don't support type editing
if (lm->nodeIdx < 0) return false; // CommandRow etc.
if (lm->lineKind == LineKind::Footer) return false;
// Position popup at the type column start // Position popup at the type column start
ColumnSpan ts = typeSpan(*lm); ColumnSpan ts = typeSpan(*lm);
long typePos = posFromCol(m_sci, line, ts.valid ? ts.start : 0); long typePos = posFromCol(m_sci, line, ts.valid ? ts.start : 0);
@@ -2234,6 +2246,10 @@ void RcxEditor::setGlobalFontName(const QString& fontName) {
g_fontName = fontName; g_fontName = fontName;
} }
QString RcxEditor::globalFontName() {
return g_fontName;
}
QString RcxEditor::textWithMargins() const { QString RcxEditor::textWithMargins() const {
int lineCount = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_GETLINECOUNT); int lineCount = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_GETLINECOUNT);
QStringList lines; QStringList lines;

View File

@@ -48,6 +48,7 @@ public:
void setCommandRowText(const QString& line); void setCommandRowText(const QString& line);
void setEditorFont(const QString& fontName); void setEditorFont(const QString& fontName);
static void setGlobalFontName(const QString& fontName); static void setGlobalFontName(const QString& fontName);
static QString globalFontName();
void applyTheme(const Theme& theme); void applyTheme(const Theme& theme);
// Custom type names (struct types from the tree) shown in type picker + lexer GlobalClass coloring // Custom type names (struct types from the tree) shown in type picker + lexer GlobalClass coloring

View File

@@ -14,7 +14,7 @@
namespace rcx { class Provider; } namespace rcx { class Provider; }
/** /**
* Plugin interface for ReclassX * Plugin interface for Reclass
* *
* Plugins are loaded from the "Plugins" folder as shared libraries. * Plugins are loaded from the "Plugins" folder as shared libraries.
* Each plugin must export a C function: extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin(); * Each plugin must export a C function: extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin();
@@ -133,4 +133,4 @@ public:
// Plugin factory function signature // Plugin factory function signature
typedef IPlugin* (*CreatePluginFunc)(); typedef IPlugin* (*CreatePluginFunc)();
#define IPLUGIN_IID "com.reclassx.IPlugin/1.0" #define IPLUGIN_IID "com.reclass.IPlugin/1.0"

View File

@@ -334,7 +334,7 @@ void MainWindow::createMenus() {
actJetBrains->setCheckable(true); actJetBrains->setCheckable(true);
actJetBrains->setActionGroup(fontGroup); actJetBrains->setActionGroup(fontGroup);
// Load saved preference // Load saved preference
QSettings settings("ReclassX", "ReclassX"); QSettings settings("Reclass", "Reclass");
QString savedFont = settings.value("font", "JetBrains Mono").toString(); QString savedFont = settings.value("font", "JetBrains Mono").toString();
if (savedFont == "JetBrains Mono") actJetBrains->setChecked(true); if (savedFont == "JetBrains Mono") actJetBrains->setChecked(true);
else actConsolas->setChecked(true); else actConsolas->setChecked(true);
@@ -392,7 +392,7 @@ void MainWindow::createStatusBar() {
statusBar()->setAutoFillBackground(true); statusBar()->setAutoFillBackground(true);
} }
QSettings settings("ReclassX", "ReclassX"); QSettings settings("Reclass", "Reclass");
QString fontName = settings.value("font", "JetBrains Mono").toString(); QString fontName = settings.value("font", "JetBrains Mono").toString();
QFont f(fontName, 12); QFont f(fontName, 12);
f.setFixedPitch(true); f.setFixedPitch(true);
@@ -400,7 +400,7 @@ void MainWindow::createStatusBar() {
} }
void MainWindow::applyTabWidgetStyle(QTabWidget* tw) { void MainWindow::applyTabWidgetStyle(QTabWidget* tw) {
QSettings settings("ReclassX", "ReclassX"); QSettings settings("Reclass", "Reclass");
QString fontName = settings.value("font", "JetBrains Mono").toString(); QString fontName = settings.value("font", "JetBrains Mono").toString();
QFont tabFont(fontName, 12); QFont tabFont(fontName, 12);
tabFont.setFixedPitch(true); tabFont.setFixedPitch(true);
@@ -889,7 +889,7 @@ void MainWindow::editTheme() {
} }
void MainWindow::setEditorFont(const QString& fontName) { void MainWindow::setEditorFont(const QString& fontName) {
QSettings settings("ReclassX", "ReclassX"); QSettings settings("Reclass", "Reclass");
settings.setValue("font", fontName); settings.setValue("font", fontName);
QFont f(fontName, 12); QFont f(fontName, 12);
f.setFixedPitch(true); f.setFixedPitch(true);
@@ -959,7 +959,7 @@ void MainWindow::updateWindowTitle() {
// ── Rendered view setup ── // ── Rendered view setup ──
void MainWindow::setupRenderedSci(QsciScintilla* sci) { void MainWindow::setupRenderedSci(QsciScintilla* sci) {
QSettings settings("ReclassX", "ReclassX"); QSettings settings("Reclass", "Reclass");
QString fontName = settings.value("font", "JetBrains Mono").toString(); QString fontName = settings.value("font", "JetBrains Mono").toString();
QFont f(fontName, 12); QFont f(fontName, 12);
f.setFixedPitch(true); f.setFixedPitch(true);
@@ -1204,7 +1204,7 @@ QMdiSubWindow* MainWindow::project_open(const QString& path) {
QString filePath = path; QString filePath = path;
if (filePath.isEmpty()) { if (filePath.isEmpty()) {
filePath = QFileDialog::getOpenFileName(this, filePath = QFileDialog::getOpenFileName(this,
"Open Definition", {}, "ReclassX (*.rcx);;JSON (*.json);;All (*)"); "Open Definition", {}, "Reclass (*.rcx);;JSON (*.json);;All (*)");
if (filePath.isEmpty()) return nullptr; if (filePath.isEmpty()) return nullptr;
} }
@@ -1226,7 +1226,7 @@ bool MainWindow::project_save(QMdiSubWindow* sub, bool saveAs) {
if (saveAs || tab.doc->filePath.isEmpty()) { if (saveAs || tab.doc->filePath.isEmpty()) {
QString path = QFileDialog::getSaveFileName(this, QString path = QFileDialog::getSaveFileName(this,
"Save Definition", {}, "ReclassX (*.rcx);;JSON (*.json)"); "Save Definition", {}, "Reclass (*.rcx);;JSON (*.json)");
if (path.isEmpty()) return false; if (path.isEmpty()) return false;
tab.doc->save(path); tab.doc->save(path);
} else { } else {
@@ -1259,7 +1259,7 @@ void MainWindow::createWorkspaceDock() {
// Match editor font // Match editor font
{ {
QSettings settings("ReclassX", "ReclassX"); QSettings settings("Reclass", "Reclass");
QString fontName = settings.value("font", "JetBrains Mono").toString(); QString fontName = settings.value("font", "JetBrains Mono").toString();
QFont f(fontName, 12); QFont f(fontName, 12);
f.setFixedPitch(true); f.setFixedPitch(true);
@@ -1442,8 +1442,8 @@ int main(int argc, char* argv[]) {
#endif #endif
DarkApp app(argc, argv); DarkApp app(argc, argv);
app.setApplicationName("ReclassX"); app.setApplicationName("Reclass");
app.setOrganizationName("ReclassX"); app.setOrganizationName("Reclass");
app.setStyle(new MenuBarStyle("Fusion")); // Fusion + generous menu sizing app.setStyle(new MenuBarStyle("Fusion")); // Fusion + generous menu sizing
// Load embedded fonts // Load embedded fonts
@@ -1452,7 +1452,7 @@ int main(int argc, char* argv[]) {
qWarning("Failed to load embedded JetBrains Mono font"); qWarning("Failed to load embedded JetBrains Mono font");
// Apply saved font preference before creating any editors // Apply saved font preference before creating any editors
{ {
QSettings settings("ReclassX", "ReclassX"); QSettings settings("Reclass", "Reclass");
QString savedFont = settings.value("font", "JetBrains Mono").toString(); QString savedFont = settings.value("font", "JetBrains Mono").toString();
rcx::RcxEditor::setGlobalFontName(savedFont); rcx::RcxEditor::setGlobalFontName(savedFont);
} }

View File

@@ -194,7 +194,7 @@ QJsonObject McpBridge::handleInitialize(const QJsonValue& id, const QJsonObject&
{"protocolVersion", "2024-11-05"}, {"protocolVersion", "2024-11-05"},
{"capabilities", caps}, {"capabilities", caps},
{"serverInfo", QJsonObject{ {"serverInfo", QJsonObject{
{"name", "reclassx-mcp"}, {"name", "reclass-mcp"},
{"version", "1.0.0"} {"version", "1.0.0"}
}} }}
}; };

View File

@@ -1,95 +1,442 @@
#include "themeeditor.h" #include "themeeditor.h"
#include <QFormLayout> #include "thememanager.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QScrollArea>
#include <QDialogButtonBox> #include <QDialogButtonBox>
#include <QColorDialog> #include <QColorDialog>
#include <QLineEdit> #include <QComboBox>
#include <QLabel> #include <cmath>
namespace rcx { namespace rcx {
ThemeEditor::ThemeEditor(const Theme& theme, QWidget* parent) // ── Color utilities ──
: QDialog(parent), m_theme(theme)
namespace {
double srgbLinear(double c) {
return (c <= 0.03928) ? c / 12.92 : std::pow((c + 0.055) / 1.055, 2.4);
}
double relativeLuminance(const QColor& c) {
return 0.2126 * srgbLinear(c.redF())
+ 0.7152 * srgbLinear(c.greenF())
+ 0.0722 * srgbLinear(c.blueF());
}
double contrastRatio(const QColor& fg, const QColor& bg) {
double l1 = relativeLuminance(fg);
double l2 = relativeLuminance(bg);
if (l1 < l2) std::swap(l1, l2);
return (l1 + 0.05) / (l2 + 0.05);
}
QString wcagLevel(double ratio) {
if (ratio >= 7.0) return QStringLiteral("AAA");
if (ratio >= 4.5) return QStringLiteral("AA");
return QStringLiteral("FAIL");
}
// Compute the minimum fg lightness (HSL L) to reach targetRatio against bg
QColor autoFixFg(const QColor& fg, const QColor& bg, double targetRatio) {
double lBg = relativeLuminance(bg);
// Determine if fg should be lighter or darker than bg
bool fgLighter = relativeLuminance(fg) >= relativeLuminance(bg);
double targetLum;
if (fgLighter)
targetLum = targetRatio * (lBg + 0.05) - 0.05;
else
targetLum = (lBg + 0.05) / targetRatio - 0.05;
targetLum = qBound(0.0, targetLum, 1.0);
// Binary search for HSL lightness that yields the target luminance
int h, s, l, a;
fg.getHsl(&h, &s, &l, &a);
int lo = fgLighter ? l : 0;
int hi = fgLighter ? 255 : l;
for (int iter = 0; iter < 20; iter++) {
int mid = (lo + hi) / 2;
QColor test;
test.setHsl(h, s, mid, a);
double testLum = relativeLuminance(test);
if (fgLighter) {
if (testLum < targetLum) lo = mid + 1;
else hi = mid;
} else {
if (testLum > targetLum) hi = mid - 1;
else lo = mid;
}
}
QColor result;
result.setHsl(h, s, fgLighter ? hi : lo, a);
return result;
}
} // anon
// ── Section header label ──
static QLabel* makeSectionLabel(const QString& text) {
auto* lbl = new QLabel(text);
lbl->setStyleSheet(QStringLiteral(
"font-weight: bold; font-size: 11px; color: #888;"
"padding: 6px 0 2px 0; border-bottom: 1px solid #444;"));
return lbl;
}
// ── Constructor ──
ThemeEditor::ThemeEditor(int themeIndex, QWidget* parent)
: QDialog(parent), m_themeIndex(themeIndex)
{ {
setWindowTitle("Edit Theme"); auto& tm = ThemeManager::instance();
setMinimumWidth(320); auto all = tm.themes();
m_theme = (themeIndex >= 0 && themeIndex < all.size()) ? all[themeIndex] : tm.current();
auto* form = new QFormLayout; setWindowTitle(QStringLiteral("Theme Editor"));
setMinimumSize(420, 480);
resize(440, 640);
// Name field auto* mainLayout = new QVBoxLayout(this);
auto* nameEdit = new QLineEdit(m_theme.name); mainLayout->setSpacing(6);
connect(nameEdit, &QLineEdit::textChanged, this, [this](const QString& t) {
// ── Theme selector combo ──
{
auto* row = new QHBoxLayout;
row->addWidget(new QLabel(QStringLiteral("Theme:")));
m_themeCombo = new QComboBox;
for (const auto& t : all)
m_themeCombo->addItem(t.name);
m_themeCombo->setCurrentIndex(themeIndex);
connect(m_themeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, [this](int idx) { loadTheme(idx); });
row->addWidget(m_themeCombo, 1);
mainLayout->addLayout(row);
}
// ── Name field ──
{
auto* row = new QHBoxLayout;
row->addWidget(new QLabel(QStringLiteral("Name:")));
m_nameEdit = new QLineEdit(m_theme.name);
connect(m_nameEdit, &QLineEdit::textChanged, this, [this](const QString& t) {
m_theme.name = t; m_theme.name = t;
}); });
form->addRow("Name", nameEdit); row->addWidget(m_nameEdit, 1);
mainLayout->addLayout(row);
}
// Color swatches // ── File info ──
m_fileInfoLabel = new QLabel;
m_fileInfoLabel->setStyleSheet(QStringLiteral("color: #666; font-size: 10px; padding: 0 0 4px 0;"));
QString path = tm.themeFilePath(themeIndex);
m_fileInfoLabel->setText(path.isEmpty()
? QStringLiteral("Built-in theme (edits save as user copy)")
: QStringLiteral("File: %1").arg(path));
mainLayout->addWidget(m_fileInfoLabel);
// ── Scrollable area for swatches + contrast ──
auto* scroll = new QScrollArea;
scroll->setWidgetResizable(true);
scroll->setFrameShape(QFrame::NoFrame);
auto* scrollWidget = new QWidget;
auto* scrollLayout = new QVBoxLayout(scrollWidget);
scrollLayout->setContentsMargins(0, 0, 6, 0); // right margin for scrollbar
scrollLayout->setSpacing(2);
// ── Color swatches ──
struct FieldDef { const char* label; QColor Theme::*ptr; }; struct FieldDef { const char* label; QColor Theme::*ptr; };
const FieldDef fields[] = {
auto addGroup = [&](const QString& title, std::initializer_list<FieldDef> fields) {
scrollLayout->addWidget(makeSectionLabel(title));
for (const auto& f : fields) {
int idx = m_swatches.size();
auto* row = new QHBoxLayout;
row->setSpacing(6);
row->setContentsMargins(8, 1, 0, 1);
auto* lbl = new QLabel(QString::fromLatin1(f.label));
lbl->setFixedWidth(120);
row->addWidget(lbl);
auto* swatchBtn = new QPushButton;
swatchBtn->setFixedSize(32, 18);
swatchBtn->setCursor(Qt::PointingHandCursor);
connect(swatchBtn, &QPushButton::clicked, this, [this, idx]() { pickColor(idx); });
row->addWidget(swatchBtn);
auto* hexLbl = new QLabel;
hexLbl->setFixedWidth(60);
hexLbl->setStyleSheet(QStringLiteral("color: #aaa; font-size: 10px;"));
row->addWidget(hexLbl);
row->addStretch();
SwatchEntry se;
se.label = f.label;
se.field = f.ptr;
se.swatchBtn = swatchBtn;
se.hexLabel = hexLbl;
m_swatches.append(se);
scrollLayout->addLayout(row);
}
};
addGroup("Chrome", {
{"Background", &Theme::background}, {"Background", &Theme::background},
{"Background Alt", &Theme::backgroundAlt}, {"Background Alt", &Theme::backgroundAlt},
{"Surface", &Theme::surface}, {"Surface", &Theme::surface},
{"Border", &Theme::border}, {"Border", &Theme::border},
{"Button", &Theme::button}, {"Button", &Theme::button},
});
addGroup("Text", {
{"Text", &Theme::text}, {"Text", &Theme::text},
{"Text Dim", &Theme::textDim}, {"Text Dim", &Theme::textDim},
{"Text Muted", &Theme::textMuted}, {"Text Muted", &Theme::textMuted},
{"Text Faint", &Theme::textFaint}, {"Text Faint", &Theme::textFaint},
});
addGroup("Interactive", {
{"Hover", &Theme::hover}, {"Hover", &Theme::hover},
{"Selected", &Theme::selected}, {"Selected", &Theme::selected},
{"Selection", &Theme::selection}, {"Selection", &Theme::selection},
});
addGroup("Syntax", {
{"Keyword", &Theme::syntaxKeyword}, {"Keyword", &Theme::syntaxKeyword},
{"Number", &Theme::syntaxNumber}, {"Number", &Theme::syntaxNumber},
{"String", &Theme::syntaxString}, {"String", &Theme::syntaxString},
{"Comment", &Theme::syntaxComment}, {"Comment", &Theme::syntaxComment},
{"Preprocessor", &Theme::syntaxPreproc}, {"Preprocessor", &Theme::syntaxPreproc},
{"Type", &Theme::syntaxType}, {"Type", &Theme::syntaxType},
});
addGroup("Indicators", {
{"Hover Span", &Theme::indHoverSpan}, {"Hover Span", &Theme::indHoverSpan},
{"Cmd Pill", &Theme::indCmdPill}, {"Cmd Pill", &Theme::indCmdPill},
{"Data Changed", &Theme::indDataChanged}, {"Data Changed", &Theme::indDataChanged},
{"Hint Green", &Theme::indHintGreen}, {"Hint Green", &Theme::indHintGreen},
{"Pointer Marker", &Theme::markerPtr}, });
{"Cycle Marker", &Theme::markerCycle}, addGroup("Markers", {
{"Error Marker", &Theme::markerError}, {"Pointer", &Theme::markerPtr},
{"Cycle", &Theme::markerCycle},
{"Error", &Theme::markerError},
});
// ── Contrast pairs ──
scrollLayout->addWidget(makeSectionLabel(QStringLiteral("Contrast")));
struct PairDef {
const char* fgLabel; const char* bgLabel;
QColor Theme::*fg; QColor Theme::*bg;
};
const PairDef pairs[] = {
{"text", "background", &Theme::text, &Theme::background},
{"textDim", "background", &Theme::textDim, &Theme::background},
{"textMuted", "background", &Theme::textMuted, &Theme::background},
{"textFaint", "background", &Theme::textFaint, &Theme::background},
{"text", "backgroundAlt", &Theme::text, &Theme::backgroundAlt},
{"text", "surface", &Theme::text, &Theme::surface},
{"keyword", "background", &Theme::syntaxKeyword, &Theme::background},
{"type", "background", &Theme::syntaxType, &Theme::background},
{"number", "background", &Theme::syntaxNumber, &Theme::background},
{"string", "background", &Theme::syntaxString, &Theme::background},
{"comment", "background", &Theme::syntaxComment, &Theme::background},
{"preproc", "background", &Theme::syntaxPreproc, &Theme::background},
{"hoverSpan", "background", &Theme::indHoverSpan, &Theme::background},
{"hintGreen", "background", &Theme::indHintGreen, &Theme::background},
}; };
for (const auto& f : fields) { for (int pi = 0; pi < (int)(sizeof(pairs) / sizeof(pairs[0])); pi++) {
auto* btn = new QPushButton; const auto& p = pairs[pi];
btn->setFixedSize(60, 24); int idx = m_contrastPairs.size();
btn->setCursor(Qt::PointingHandCursor);
SwatchEntry entry{f.label, f.ptr, btn};
m_swatches.append(entry);
updateSwatch(m_swatches.last());
int idx = m_swatches.size() - 1; auto* row = new QHBoxLayout;
connect(btn, &QPushButton::clicked, this, [this, idx]() { row->setSpacing(4);
pickColor(m_swatches[idx]); row->setContentsMargins(8, 1, 0, 1);
});
form->addRow(f.label, btn); auto* pairLabel = new QLabel(QStringLiteral("%1 / %2")
.arg(QString::fromLatin1(p.fgLabel), QString::fromLatin1(p.bgLabel)));
pairLabel->setFixedWidth(150);
pairLabel->setStyleSheet(QStringLiteral("font-size: 10px;"));
row->addWidget(pairLabel);
auto* ratioLbl = new QLabel;
ratioLbl->setFixedWidth(44);
ratioLbl->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
ratioLbl->setStyleSheet(QStringLiteral("font-size: 10px;"));
row->addWidget(ratioLbl);
auto* levelLbl = new QLabel;
levelLbl->setFixedWidth(34);
levelLbl->setAlignment(Qt::AlignCenter);
row->addWidget(levelLbl);
auto* fixBtn = new QPushButton(QStringLiteral("Fix"));
fixBtn->setFixedSize(36, 18);
fixBtn->setCursor(Qt::PointingHandCursor);
fixBtn->setStyleSheet(QStringLiteral(
"QPushButton { font-size: 9px; padding: 0; border: 1px solid #555; border-radius: 2px; }"
"QPushButton:hover { background: #444; }"));
fixBtn->hide();
connect(fixBtn, &QPushButton::clicked, this, [this, idx]() { autoFixContrast(idx); });
row->addWidget(fixBtn);
row->addStretch();
ContrastEntry ce;
ce.fgLabel = p.fgLabel;
ce.bgLabel = p.bgLabel;
ce.fgField = p.fg;
ce.bgField = p.bg;
ce.ratioLabel = ratioLbl;
ce.levelLabel = levelLbl;
ce.fixBtn = fixBtn;
m_contrastPairs.append(ce);
scrollLayout->addLayout(row);
} }
scrollLayout->addStretch();
scroll->setWidget(scrollWidget);
mainLayout->addWidget(scroll, 1);
// ── Bottom bar ──
auto* bottomRow = new QHBoxLayout;
m_previewBtn = new QPushButton(QStringLiteral("Live Preview"));
m_previewBtn->setCheckable(true);
connect(m_previewBtn, &QPushButton::toggled, this, [this](bool) { togglePreview(); });
bottomRow->addWidget(m_previewBtn);
bottomRow->addStretch();
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); connect(buttons, &QDialogButtonBox::rejected, this, [this]() {
if (m_previewing) {
ThemeManager::instance().revertPreview();
m_previewing = false;
}
reject();
});
bottomRow->addWidget(buttons);
mainLayout->addLayout(bottomRow);
auto* layout = new QVBoxLayout(this); // Initial update
layout->addLayout(form); for (int i = 0; i < m_swatches.size(); i++)
layout->addWidget(buttons); updateSwatch(i);
updateAllContrast();
} }
void ThemeEditor::updateSwatch(SwatchEntry& entry) { // ── Load a different theme into the editor ──
QColor c = m_theme.*entry.field;
entry.button->setStyleSheet(QStringLiteral( void ThemeEditor::loadTheme(int index) {
"QPushButton { background: %1; border: 1px solid #555; border-radius: 3px; }") auto& tm = ThemeManager::instance();
auto all = tm.themes();
if (index < 0 || index >= all.size()) return;
m_themeIndex = index;
m_theme = all[index];
m_nameEdit->setText(m_theme.name);
QString path = tm.themeFilePath(index);
m_fileInfoLabel->setText(path.isEmpty()
? QStringLiteral("Built-in theme (edits save as user copy)")
: QStringLiteral("File: %1").arg(path));
for (int i = 0; i < m_swatches.size(); i++)
updateSwatch(i);
updateAllContrast();
if (m_previewing)
tm.previewTheme(m_theme);
}
// ── Swatch update ──
void ThemeEditor::updateSwatch(int idx) {
auto& s = m_swatches[idx];
QColor c = m_theme.*s.field;
s.swatchBtn->setStyleSheet(QStringLiteral(
"QPushButton { background: %1; border: 1px solid #555; border-radius: 2px; }")
.arg(c.name())); .arg(c.name()));
entry.button->setToolTip(c.name()); s.hexLabel->setText(c.name());
} }
void ThemeEditor::pickColor(SwatchEntry& entry) { // ── Contrast update ──
QColor c = QColorDialog::getColor(m_theme.*entry.field, this, entry.label);
if (c.isValid()) { void ThemeEditor::updateAllContrast() {
m_theme.*entry.field = c; for (int i = 0; i < m_contrastPairs.size(); i++) {
updateSwatch(entry); auto& cp = m_contrastPairs[i];
QColor fg = m_theme.*cp.fgField;
QColor bg = m_theme.*cp.bgField;
double ratio = contrastRatio(fg, bg);
QString level = wcagLevel(ratio);
cp.ratioLabel->setText(QStringLiteral("%1:1").arg(ratio, 0, 'f', 1));
cp.levelLabel->setText(level);
if (level == "AAA")
cp.levelLabel->setStyleSheet(QStringLiteral("color: #4ec94e; font-weight: bold; font-size: 10px;"));
else if (level == "AA")
cp.levelLabel->setStyleSheet(QStringLiteral("color: #c9c94e; font-weight: bold; font-size: 10px;"));
else
cp.levelLabel->setStyleSheet(QStringLiteral("color: #c94e4e; font-weight: bold; font-size: 10px;"));
cp.fixBtn->setVisible(level == "FAIL");
} }
} }
// ── Color picker ──
void ThemeEditor::pickColor(int idx) {
auto& s = m_swatches[idx];
QColor c = QColorDialog::getColor(m_theme.*s.field, this, QString::fromLatin1(s.label));
if (c.isValid()) {
m_theme.*s.field = c;
updateSwatch(idx);
updateAllContrast();
if (m_previewing)
ThemeManager::instance().previewTheme(m_theme);
}
}
// ── Auto-fix contrast ──
void ThemeEditor::autoFixContrast(int idx) {
auto& cp = m_contrastPairs[idx];
QColor fg = m_theme.*cp.fgField;
QColor bg = m_theme.*cp.bgField;
QColor fixed = autoFixFg(fg, bg, 4.6); // slightly above 4.5 for margin
m_theme.*cp.fgField = fixed;
// Update the swatch that owns this fg color
for (int i = 0; i < m_swatches.size(); i++) {
if (m_swatches[i].field == cp.fgField) {
updateSwatch(i);
break;
}
}
updateAllContrast();
if (m_previewing)
ThemeManager::instance().previewTheme(m_theme);
}
// ── Live preview toggle ──
void ThemeEditor::togglePreview() {
m_previewing = m_previewBtn->isChecked();
if (m_previewing)
ThemeManager::instance().previewTheme(m_theme);
else
ThemeManager::instance().revertPreview();
}
} // namespace rcx } // namespace rcx

View File

@@ -3,27 +3,61 @@
#include <QDialog> #include <QDialog>
#include <QVector> #include <QVector>
#include <QPushButton> #include <QPushButton>
#include <QLabel>
#include <QLineEdit>
class QScrollArea;
class QVBoxLayout;
class QComboBox;
namespace rcx { namespace rcx {
class ThemeEditor : public QDialog { class ThemeEditor : public QDialog {
Q_OBJECT Q_OBJECT
public: public:
explicit ThemeEditor(const Theme& theme, QWidget* parent = nullptr); explicit ThemeEditor(int themeIndex, QWidget* parent = nullptr);
Theme result() const { return m_theme; } Theme result() const { return m_theme; }
int selectedIndex() const { return m_themeIndex; }
private: private:
Theme m_theme; Theme m_theme;
int m_themeIndex;
// ── Swatch row (compact: label + swatch + hex) ──
struct SwatchEntry { struct SwatchEntry {
const char* label; const char* label;
QColor Theme::*field; QColor Theme::*field;
QPushButton* button; QPushButton* swatchBtn = nullptr;
QLabel* hexLabel = nullptr;
}; };
QVector<SwatchEntry> m_swatches; QVector<SwatchEntry> m_swatches;
void updateSwatch(SwatchEntry& entry); // ── Contrast pair row ──
void pickColor(SwatchEntry& entry); struct ContrastEntry {
const char* fgLabel;
const char* bgLabel;
QColor Theme::*fgField;
QColor Theme::*bgField;
QLabel* ratioLabel = nullptr;
QLabel* levelLabel = nullptr;
QPushButton* fixBtn = nullptr;
};
QVector<ContrastEntry> m_contrastPairs;
// ── UI ──
QComboBox* m_themeCombo = nullptr;
QLineEdit* m_nameEdit = nullptr;
QLabel* m_fileInfoLabel = nullptr;
QPushButton* m_previewBtn = nullptr;
bool m_previewing = false;
void loadTheme(int index);
void rebuildSwatches(QVBoxLayout* swatchLayout);
void updateSwatch(int idx);
void updateAllContrast();
void pickColor(int idx);
void autoFixContrast(int idx);
void togglePreview();
}; };
} // namespace rcx } // namespace rcx

View File

@@ -17,7 +17,7 @@ ThemeManager::ThemeManager() {
m_builtIn.append(Theme::warm()); m_builtIn.append(Theme::warm());
loadUserThemes(); loadUserThemes();
QSettings settings("ReclassX", "ReclassX"); QSettings settings("Reclass", "Reclass");
QString saved = settings.value("theme", m_builtIn[0].name).toString(); QString saved = settings.value("theme", m_builtIn[0].name).toString();
auto all = themes(); auto all = themes();
for (int i = 0; i < all.size(); i++) { for (int i = 0; i < all.size(); i++) {
@@ -44,7 +44,7 @@ void ThemeManager::setCurrent(int index) {
auto all = themes(); auto all = themes();
if (index < 0 || index >= all.size()) return; if (index < 0 || index >= all.size()) return;
m_currentIdx = index; m_currentIdx = index;
QSettings settings("ReclassX", "ReclassX"); QSettings settings("Reclass", "Reclass");
settings.setValue("theme", all[index].name); settings.setValue("theme", all[index].name);
emit themeChanged(current()); emit themeChanged(current());
} }
@@ -116,4 +116,27 @@ void ThemeManager::saveUserThemes() const {
} }
} }
QString ThemeManager::themeFilePath(int index) const {
if (index < builtInCount()) return {};
int ui = index - builtInCount();
if (ui < 0 || ui >= m_user.size()) return {};
QString filename = m_user[ui].name.toLower().replace(' ', '_') + ".json";
return themesDir() + "/" + filename;
}
void ThemeManager::previewTheme(const Theme& theme) {
if (!m_previewing) {
m_savedTheme = current();
m_previewing = true;
}
emit themeChanged(theme);
}
void ThemeManager::revertPreview() {
if (m_previewing) {
m_previewing = false;
emit themeChanged(m_savedTheme);
}
}
} // namespace rcx } // namespace rcx

View File

@@ -22,6 +22,10 @@ public:
void loadUserThemes(); void loadUserThemes();
void saveUserThemes() const; void saveUserThemes() const;
QString themeFilePath(int index) const;
void previewTheme(const Theme& theme);
void revertPreview();
signals: signals:
void themeChanged(const rcx::Theme& theme); void themeChanged(const rcx::Theme& theme);
@@ -33,6 +37,8 @@ private:
int builtInCount() const { return m_builtIn.size(); } int builtInCount() const { return m_builtIn.size(); }
QString themesDir() const; QString themesDir() const;
bool m_previewing = false;
Theme m_savedTheme; // stashed current theme during preview
}; };
} // namespace rcx } // namespace rcx

View File

@@ -1,9 +1,9 @@
// rcx-mcp-stdio: Bridges stdin/stdout to QLocalSocket for MCP transport. // rcx-mcp-stdio: Bridges stdin/stdout to QLocalSocket for MCP transport.
// Claude Desktop spawns this process; it connects to the rcx-mcp named pipe // Claude Desktop spawns this process; it connects to the rcx-mcp named pipe
// inside the running ReclassX application. // inside the running Reclass application.
// //
// stdin (from Claude) → QLocalSocket → McpBridge (in ReclassX) // stdin (from Claude) → QLocalSocket → McpBridge (in Reclass)
// stdout (to Claude) ← QLocalSocket ← McpBridge (in ReclassX) // stdout (to Claude) ← QLocalSocket ← McpBridge (in Reclass)
#include <QCoreApplication> #include <QCoreApplication>
#include <QLocalSocket> #include <QLocalSocket>
@@ -29,7 +29,7 @@ int main(int argc, char* argv[]) {
auto* socket = new QLocalSocket(&app); auto* socket = new QLocalSocket(&app);
QByteArray readBuf; QByteArray readBuf;
// Socket → stdout: forward lines from ReclassX to Claude Desktop // Socket → stdout: forward lines from Reclass to Claude Desktop
QObject::connect(socket, &QLocalSocket::readyRead, [&]() { QObject::connect(socket, &QLocalSocket::readyRead, [&]() {
readBuf.append(socket->readAll()); readBuf.append(socket->readAll());
while (true) { while (true) {