From 3ab6425fc5e416f5148ee226ff90b7ec06009371 Mon Sep 17 00:00:00 2001 From: Mike Rodriguez Date: Mon, 31 Aug 2026 02:22:37 -0700 Subject: [PATCH 01/10] Harden monitor lifetime and mode handling --- .../MttVDD/Driver.cpp | 403 +++++++++++++++--- Virtual Display Driver (HDR)/MttVDD/Driver.h | 41 +- 2 files changed, 392 insertions(+), 52 deletions(-) diff --git a/Virtual Display Driver (HDR)/MttVDD/Driver.cpp b/Virtual Display Driver (HDR)/MttVDD/Driver.cpp index 3145b04a..a5cc1ae6 100644 --- a/Virtual Display Driver (HDR)/MttVDD/Driver.cpp +++ b/Virtual Display Driver (HDR)/MttVDD/Driver.cpp @@ -314,6 +314,17 @@ struct IndirectDeviceContextWrapper pContext = nullptr; } }; + +struct IndirectMonitorContextWrapper +{ + IndirectMonitorContext* pContext; + + void Cleanup() + { + delete pContext; + pContext = nullptr; + } +}; void LogQueries(const char* severity, const std::wstring& xmlName) { if (xmlName.find(L"logging") == std::wstring::npos) { int size_needed = WideCharToMultiByte(CP_UTF8, 0, xmlName.c_str(), (int)xmlName.size(), NULL, 0, NULL, NULL); @@ -1633,6 +1644,42 @@ void InitializeD3DDeviceAndLogGPU() { // This macro creates the methods for accessing an IndirectDeviceContextWrapper as a context for a WDF object WDF_DECLARE_CONTEXT_TYPE(IndirectDeviceContextWrapper); +WDF_DECLARE_CONTEXT_TYPE(IndirectMonitorContextWrapper); + +namespace +{ + class IddCxMonitorRefScope + { + public: + explicit IddCxMonitorRefScope(IDDCX_MONITOR monitor) + : m_Object(reinterpret_cast(monitor)) + { + WdfObjectReference(m_Object); + } + + ~IddCxMonitorRefScope() + { + WdfObjectDereference(m_Object); + } + + IddCxMonitorRefScope(const IddCxMonitorRefScope&) = delete; + IddCxMonitorRefScope& operator=(const IddCxMonitorRefScope&) = delete; + + private: + WDFOBJECT m_Object; + }; + + IndirectMonitorContext* GetMonitorContextIfReady(IDDCX_MONITOR monitorObject) + { + auto* wrapper = WdfObjectGet_IndirectMonitorContextWrapper(monitorObject); + if (wrapper == nullptr || wrapper->pContext == nullptr) + { + return nullptr; + } + + return wrapper->pContext; + } +} extern "C" BOOL WINAPI DllMain( _In_ HINSTANCE hInstance, @@ -3650,11 +3697,98 @@ void IndirectDeviceContext::CleanupExpiredDevices() } } +IndirectMonitorContext::IndirectMonitorContext( + _In_ IndirectDeviceContext* DeviceContext, + _In_ IDDCX_MONITOR Monitor, + _In_ UINT ConnectorIndex) : + m_DeviceContext(DeviceContext), + m_Monitor(Monitor), + m_ConnectorIndex(ConnectorIndex), + m_hCursorEvent(nullptr), + m_PathActive(false), + m_HasCommittedTargetMode(false), + m_CommittedTargetSignal({}) +{ +} + +IndirectMonitorContext::~IndirectMonitorContext() +{ + if (m_DeviceContext != nullptr) + { + m_DeviceContext->UnassignSwapChain(this); + } + ClearCursorEvent(); +} + +IndirectDeviceContext* IndirectMonitorContext::GetDeviceContext() const +{ + return m_DeviceContext; +} + +IDDCX_MONITOR IndirectMonitorContext::GetMonitor() const +{ + return m_Monitor; +} + +UINT IndirectMonitorContext::GetConnectorIndex() const +{ + return m_ConnectorIndex; +} + +void IndirectMonitorContext::ApplyCommittedPath( + _In_ IDDCX_PATH_FLAGS Flags, + _In_ const DISPLAYCONFIG_VIDEO_SIGNAL_INFO& TargetSignal) +{ + bool shouldUnassign = false; + { + lock_guard lock(m_StateMutex); + if ((Flags & IDDCX_PATH_FLAGS_ACTIVE) == 0) + { + m_PathActive = false; + shouldUnassign = true; + } + else if (TargetSignal.activeSize.cx == 0 || TargetSignal.activeSize.cy == 0) + { + vddlog("w", "Ignoring an active committed path with zero dimensions."); + return; + } + else + { + m_CommittedTargetSignal = TargetSignal; + m_HasCommittedTargetMode = true; + m_PathActive = true; + } + } + + if (shouldUnassign && m_DeviceContext != nullptr) + { + m_DeviceContext->UnassignSwapChain(this); + } +} + +void IndirectMonitorContext::ReplaceCursorEvent(_In_opt_ HANDLE CursorEvent) +{ + HANDLE oldEvent = nullptr; + { + lock_guard lock(m_StateMutex); + oldEvent = m_hCursorEvent; + m_hCursorEvent = CursorEvent; + } + + if (oldEvent != nullptr && oldEvent != INVALID_HANDLE_VALUE) + { + CloseHandle(oldEvent); + } +} + +void IndirectMonitorContext::ClearCursorEvent() +{ + ReplaceCursorEvent(nullptr); +} + IndirectDeviceContext::IndirectDeviceContext(_In_ WDFDEVICE WdfDevice) : m_WdfDevice(WdfDevice), - m_Adapter(nullptr), - m_Monitor(nullptr), - m_Monitor2(nullptr) + m_Adapter(nullptr) { // Initialize Phase 5: Final Integration and Testing NTSTATUS initStatus = InitializePhase5Integration(); @@ -3795,7 +3929,15 @@ void IndirectDeviceContext::CreateMonitor(unsigned int index) { // ============================== WDF_OBJECT_ATTRIBUTES Attr; - WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&Attr, IndirectDeviceContextWrapper); + WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&Attr, IndirectMonitorContextWrapper); + Attr.EvtCleanupCallback = [](WDFOBJECT Object) + { + auto* wrapper = WdfObjectGet_IndirectMonitorContextWrapper(Object); + if (wrapper != nullptr) + { + wrapper->Cleanup(); + } + }; IDDCX_MONITOR_INFO MonitorInfo = {}; MonitorInfo.Size = sizeof(MonitorInfo); @@ -3845,15 +3987,28 @@ void IndirectDeviceContext::CreateMonitor(unsigned int index) { if (NT_SUCCESS(Status)) { vddlog("d", "Monitor created successfully."); - m_Monitor = MonitorCreateOut.MonitorObject; + IDDCX_MONITOR monitorObject = MonitorCreateOut.MonitorObject; - // Associate the monitor with this device context - auto* pContext = WdfObjectGet_IndirectDeviceContextWrapper(MonitorCreateOut.MonitorObject); - pContext->pContext = this; + // Associate monitor-specific state with the WDF monitor object. + auto* monitorWrapper = WdfObjectGet_IndirectMonitorContextWrapper(monitorObject); + if (monitorWrapper == nullptr) + { + vddlog("e", "Failed to get the monitor context wrapper."); + WdfObjectDelete(monitorObject); + return; + } + + monitorWrapper->pContext = new (nothrow) IndirectMonitorContext(this, monitorObject, index); + if (monitorWrapper->pContext == nullptr) + { + vddlog("e", "Failed to allocate the monitor runtime context."); + WdfObjectDelete(monitorObject); + return; + } // Tell the OS that the monitor has been plugged in IDARG_OUT_MONITORARRIVAL ArrivalOut; - Status = IddCxMonitorArrival(m_Monitor, &ArrivalOut); + Status = IddCxMonitorArrival(monitorObject, &ArrivalOut); if (NT_SUCCESS(Status)) { vddlog("d", "Monitor arrival successfully reported."); @@ -3863,6 +4018,13 @@ void IndirectDeviceContext::CreateMonitor(unsigned int index) { stringstream ss; ss << "Failed to report monitor arrival. Status: " << Status; vddlog("e", ss.str().c_str()); + + // Arrival failure leaves no usable monitor. Tear down the context and + // WDF object so later callbacks cannot observe a half-created monitor. + IndirectMonitorContext* failedContext = monitorWrapper->pContext; + monitorWrapper->pContext = nullptr; + delete failedContext; + WdfObjectDelete(monitorObject); } } else @@ -3873,8 +4035,18 @@ void IndirectDeviceContext::CreateMonitor(unsigned int index) { } } -void IndirectDeviceContext::AssignSwapChain(IDDCX_MONITOR Monitor, IDDCX_SWAPCHAIN SwapChain, LUID RenderAdapter, HANDLE NewFrameEvent) +void IndirectDeviceContext::AssignSwapChain(IndirectMonitorContext* MonitorContext, IDDCX_SWAPCHAIN SwapChain, LUID RenderAdapter, HANDLE NewFrameEvent) { + if (MonitorContext == nullptr) + { + vddlog("e", "Cannot assign a swap chain without a monitor context."); + WdfObjectDelete(SwapChain); + return; + } + + IDDCX_MONITOR Monitor = MonitorContext->GetMonitor(); + MonitorContext->ClearCursorEvent(); + // Only cleanup expired devices periodically, not on every assignment static int assignmentCount = 0; if (++assignmentCount % 10 == 0) { @@ -3912,7 +4084,7 @@ void IndirectDeviceContext::AssignSwapChain(IDDCX_MONITOR Monitor, IDDCX_SWAPCHA nullptr, false, false, - "VirtualDisplayDriverMouse" + nullptr ); if (!mouseEvent) @@ -3941,12 +4113,14 @@ void IndirectDeviceContext::AssignSwapChain(IDDCX_MONITOR Monitor, IDDCX_SWAPCHA &hwCursor ); - if (FAILED(Status)) + if (!NT_SUCCESS(Status)) { CloseHandle(mouseEvent); return; } + MonitorContext->ReplaceCursorEvent(mouseEvent); + vddlog("d", "Hardware cursor setup completed successfully."); } else { @@ -3958,8 +4132,14 @@ void IndirectDeviceContext::AssignSwapChain(IDDCX_MONITOR Monitor, IDDCX_SWAPCHA } -void IndirectDeviceContext::UnassignSwapChain(IDDCX_MONITOR Monitor) +void IndirectDeviceContext::UnassignSwapChain(IndirectMonitorContext* MonitorContext) { + if (MonitorContext == nullptr) + { + return; + } + + IDDCX_MONITOR Monitor = MonitorContext->GetMonitor(); std::unique_ptr processorToStop; { @@ -3980,6 +4160,8 @@ void IndirectDeviceContext::UnassignSwapChain(IDDCX_MONITOR Monitor) { vddlog("w", "UnassignSwapChain called for a monitor without an active processing thread."); } + + MonitorContext->ClearCursorEvent(); } #pragma endregion @@ -3992,21 +4174,30 @@ NTSTATUS VirtualDisplayDriverAdapterInitFinished(IDDCX_ADAPTER AdapterObject, co // This is called when the OS has finished setting up the adapter for use by the IddCx driver. It's now possible // to report attached monitors. - auto* pContext = WdfObjectGet_IndirectDeviceContextWrapper(AdapterObject); - if (NT_SUCCESS(pInArgs->AdapterInitStatus)) + if (pInArgs == nullptr) { - pContext->pContext->FinishInit(); - vddlog("d", "Adapter initialization finished successfully."); + vddlog("e", "Adapter initialization callback received null input arguments."); + return STATUS_INVALID_PARAMETER; } - else + + if (!NT_SUCCESS(pInArgs->AdapterInitStatus)) { stringstream ss; ss << "Adapter initialization failed. Status: " << pInArgs->AdapterInitStatus; vddlog("e", ss.str().c_str()); + return pInArgs->AdapterInitStatus; + } + + auto* pContext = WdfObjectGet_IndirectDeviceContextWrapper(AdapterObject); + if (pContext == nullptr || pContext->pContext == nullptr) + { + vddlog("e", "Adapter initialization completed without a valid device context."); + return STATUS_INVALID_DEVICE_STATE; } - vddlog("i", "Finished Setting up adapter."); - + pContext->pContext->FinishInit(); + vddlog("d", "Adapter initialization finished successfully."); + vddlog("i", "Finished Setting up adapter."); return STATUS_SUCCESS; } @@ -4014,15 +4205,22 @@ _Use_decl_annotations_ NTSTATUS VirtualDisplayDriverAdapterCommitModes(IDDCX_ADAPTER AdapterObject, const IDARG_IN_COMMITMODES* pInArgs) { UNREFERENCED_PARAMETER(AdapterObject); - UNREFERENCED_PARAMETER(pInArgs); - // For the sample, do nothing when modes are picked - the swap-chain is taken care of by IddCx + if (pInArgs == nullptr || (pInArgs->PathCount != 0 && pInArgs->pPaths == nullptr)) + { + return STATUS_INVALID_PARAMETER; + } - // ============================== - // TODO: In a real driver, this function would be used to reconfigure the device to commit the new modes. Loop - // through pInArgs->pPaths and look for IDDCX_PATH_FLAGS_ACTIVE. Any path not active is inactive (e.g. the monitor - // should be turned off). - // ============================== + for (UINT pathIndex = 0; pathIndex < pInArgs->PathCount; ++pathIndex) + { + const IDDCX_PATH& path = pInArgs->pPaths[pathIndex]; + IddCxMonitorRefScope monitorRef(path.MonitorObject); + auto* monitorContext = GetMonitorContextIfReady(path.MonitorObject); + if (monitorContext != nullptr) + { + monitorContext->ApplyCommittedPath(path.Flags, path.TargetVideoSignalInfo); + } + } return STATUS_SUCCESS; } @@ -4034,6 +4232,11 @@ NTSTATUS VirtualDisplayDriverParseMonitorDescription(const IDARG_IN_PARSEMONITOR // this sample driver, we hard-code the EDID, so this function can generate known modes. // ============================== + if (pInArgs == nullptr || pOutArgs == nullptr) + { + return STATUS_INVALID_PARAMETER; + } + stringstream logStream; logStream << "Parsing monitor description. Input buffer count: " << pInArgs->MonitorModeBufferInputCount; vddlog("d", logStream.str().c_str()); @@ -4055,6 +4258,11 @@ NTSTATUS VirtualDisplayDriverParseMonitorDescription(const IDARG_IN_PARSEMONITOR } else { + if (pInArgs->pMonitorModes == nullptr) + { + return STATUS_INVALID_PARAMETER; + } + // Copy the known modes to the output buffer for (DWORD ModeIndex = 0; ModeIndex < monitorModes.size(); ModeIndex++) { @@ -4073,7 +4281,12 @@ NTSTATUS VirtualDisplayDriverParseMonitorDescription(const IDARG_IN_PARSEMONITOR _Use_decl_annotations_ NTSTATUS VirtualDisplayDriverMonitorGetDefaultModes(IDDCX_MONITOR MonitorObject, const IDARG_IN_GETDEFAULTDESCRIPTIONMODES* pInArgs, IDARG_OUT_GETDEFAULTDESCRIPTIONMODES* pOutArgs) { - UNREFERENCED_PARAMETER(MonitorObject); + IddCxMonitorRefScope monitorRef(MonitorObject); + if (GetMonitorContextIfReady(MonitorObject) == nullptr) + { + return STATUS_INVALID_DEVICE_STATE; + } + UNREFERENCED_PARAMETER(pInArgs); UNREFERENCED_PARAMETER(pOutArgs); @@ -4170,7 +4383,16 @@ void CreateTargetMode2(IDDCX_TARGET_MODE2& Mode, UINT Width, UINT Height, UINT V _Use_decl_annotations_ NTSTATUS VirtualDisplayDriverMonitorQueryModes(IDDCX_MONITOR MonitorObject, const IDARG_IN_QUERYTARGETMODES* pInArgs, IDARG_OUT_QUERYTARGETMODES* pOutArgs)//////////////////////////////////////////////////////////////////////////////// { - UNREFERENCED_PARAMETER(MonitorObject); + if (pInArgs == nullptr || pOutArgs == nullptr) + { + return STATUS_INVALID_PARAMETER; + } + + IddCxMonitorRefScope monitorRef(MonitorObject); + if (GetMonitorContextIfReady(MonitorObject) == nullptr) + { + return STATUS_INVALID_DEVICE_STATE; + } vector TargetModes(monitorModes.size()); @@ -4198,18 +4420,28 @@ NTSTATUS VirtualDisplayDriverMonitorQueryModes(IDDCX_MONITOR MonitorObject, cons logStream << "Number of target modes to output: " << pOutArgs->TargetModeBufferOutputCount; vddlog("d", logStream.str().c_str()); - if (pInArgs->TargetModeBufferInputCount >= TargetModes.size()) + if (pInArgs->TargetModeBufferInputCount == 0) { - logStream.str(""); - logStream << "Copying target modes to output buffer."; - vddlog("d", logStream.str().c_str()); - copy(TargetModes.begin(), TargetModes.end(), pInArgs->pTargetModes); + return STATUS_SUCCESS; } - else { + else if (pInArgs->TargetModeBufferInputCount < TargetModes.size()) + { logStream.str(""); logStream << "Input buffer too small. Required: " << TargetModes.size() << ", Provided: " << pInArgs->TargetModeBufferInputCount; vddlog("w", logStream.str().c_str()); + return STATUS_BUFFER_TOO_SMALL; + } + else if (pInArgs->pTargetModes == nullptr) + { + return STATUS_INVALID_PARAMETER; + } + else + { + logStream.str(""); + logStream << "Copying target modes to output buffer."; + vddlog("d", logStream.str().c_str()); + copy(TargetModes.begin(), TargetModes.end(), pInArgs->pTargetModes); } return STATUS_SUCCESS; @@ -4218,14 +4450,25 @@ NTSTATUS VirtualDisplayDriverMonitorQueryModes(IDDCX_MONITOR MonitorObject, cons _Use_decl_annotations_ NTSTATUS VirtualDisplayDriverMonitorAssignSwapChain(IDDCX_MONITOR MonitorObject, const IDARG_IN_SETSWAPCHAIN* pInArgs) { + if (pInArgs == nullptr) + { + return STATUS_INVALID_PARAMETER; + } + + IddCxMonitorRefScope monitorRef(MonitorObject); + auto* monitorContext = GetMonitorContextIfReady(MonitorObject); + if (monitorContext == nullptr || monitorContext->GetDeviceContext() == nullptr) + { + return STATUS_INVALID_DEVICE_STATE; + } + stringstream logStream; logStream << "Assigning swap chain:" << "\n hSwapChain: " << pInArgs->hSwapChain << "\n RenderAdapterLuid: " << pInArgs->RenderAdapterLuid.LowPart << "-" << pInArgs->RenderAdapterLuid.HighPart << "\n hNextSurfaceAvailable: " << pInArgs->hNextSurfaceAvailable; vddlog("d", logStream.str().c_str()); - auto* pContext = WdfObjectGet_IndirectDeviceContextWrapper(MonitorObject); - pContext->pContext->AssignSwapChain(MonitorObject, pInArgs->hSwapChain, pInArgs->RenderAdapterLuid, pInArgs->hNextSurfaceAvailable); + monitorContext->GetDeviceContext()->AssignSwapChain(monitorContext, pInArgs->hSwapChain, pInArgs->RenderAdapterLuid, pInArgs->hNextSurfaceAvailable); vddlog("d", "Swap chain assigned successfully."); return STATUS_SUCCESS; } @@ -4233,11 +4476,17 @@ NTSTATUS VirtualDisplayDriverMonitorAssignSwapChain(IDDCX_MONITOR MonitorObject, _Use_decl_annotations_ NTSTATUS VirtualDisplayDriverMonitorUnassignSwapChain(IDDCX_MONITOR MonitorObject) { + IddCxMonitorRefScope monitorRef(MonitorObject); + auto* monitorContext = GetMonitorContextIfReady(MonitorObject); + if (monitorContext == nullptr || monitorContext->GetDeviceContext() == nullptr) + { + return STATUS_INVALID_DEVICE_STATE; + } + stringstream logStream; logStream << "Unassigning swap chain for monitor object: " << MonitorObject; vddlog("d", logStream.str().c_str()); - auto* pContext = WdfObjectGet_IndirectDeviceContextWrapper(MonitorObject); - pContext->pContext->UnassignSwapChain(MonitorObject); + monitorContext->GetDeviceContext()->UnassignSwapChain(monitorContext); vddlog("d", "Swap chain unassigned successfully."); return STATUS_SUCCESS; } @@ -4287,7 +4536,16 @@ NTSTATUS VirtualDisplayDriverEvtIddCxMonitorSetDefaultHdrMetadata( const IDARG_IN_MONITOR_SET_DEFAULT_HDR_METADATA* pInArgs ) { - UNREFERENCED_PARAMETER(pInArgs); + if (pInArgs == nullptr) + { + return STATUS_INVALID_PARAMETER; + } + + IddCxMonitorRefScope monitorRef(MonitorObject); + if (GetMonitorContextIfReady(MonitorObject) == nullptr) + { + return STATUS_INVALID_DEVICE_STATE; + } stringstream logStream; logStream << "=== PROCESSING HDR METADATA REQUEST ==="; @@ -4387,6 +4645,11 @@ NTSTATUS VirtualDisplayDriverEvtIddCxParseMonitorDescription2( // this sample driver, we hard-code the EDID, so this function can generate known modes. // ============================== + if (pInArgs == nullptr || pOutArgs == nullptr) + { + return STATUS_INVALID_PARAMETER; + } + stringstream logStream; logStream << "Parsing monitor description:" << "\n MonitorModeBufferInputCount: " << pInArgs->MonitorModeBufferInputCount @@ -4469,7 +4732,17 @@ NTSTATUS VirtualDisplayDriverEvtIddCxMonitorQueryTargetModes2( IDARG_OUT_QUERYTARGETMODES* pOutArgs ) { - //UNREFERENCED_PARAMETER(MonitorObject); + if (pInArgs == nullptr || pOutArgs == nullptr) + { + return STATUS_INVALID_PARAMETER; + } + + IddCxMonitorRefScope monitorRef(MonitorObject); + if (GetMonitorContextIfReady(MonitorObject) == nullptr) + { + return STATUS_INVALID_DEVICE_STATE; + } + stringstream logStream; logStream << "Querying target modes:" @@ -4501,7 +4774,20 @@ NTSTATUS VirtualDisplayDriverEvtIddCxMonitorQueryTargetModes2( logStream << "Output target modes count: " << pOutArgs->TargetModeBufferOutputCount; vddlog("d", logStream.str().c_str()); - if (pInArgs->TargetModeBufferInputCount >= TargetModes.size()) + if (pInArgs->TargetModeBufferInputCount == 0) + { + return STATUS_SUCCESS; + } + else if (pInArgs->TargetModeBufferInputCount < TargetModes.size()) + { + vddlog("w", "Input buffer is too small for target modes."); + return STATUS_BUFFER_TOO_SMALL; + } + else if (pInArgs->pTargetModes == nullptr) + { + return STATUS_INVALID_PARAMETER; + } + else { copy(TargetModes.begin(), TargetModes.end(), pInArgs->pTargetModes); @@ -4515,11 +4801,6 @@ NTSTATUS VirtualDisplayDriverEvtIddCxMonitorQueryTargetModes2( } vddlog("d", logStream.str().c_str()); } - else - { - vddlog("w", "Input buffer is too small for target modes."); - } - return STATUS_SUCCESS; } @@ -4530,7 +4811,22 @@ NTSTATUS VirtualDisplayDriverEvtIddCxAdapterCommitModes2( ) { UNREFERENCED_PARAMETER(AdapterObject); - UNREFERENCED_PARAMETER(pInArgs); + + if (pInArgs == nullptr || (pInArgs->PathCount != 0 && pInArgs->pPaths == nullptr)) + { + return STATUS_INVALID_PARAMETER; + } + + for (UINT pathIndex = 0; pathIndex < pInArgs->PathCount; ++pathIndex) + { + const IDDCX_PATH2& path = pInArgs->pPaths[pathIndex]; + IddCxMonitorRefScope monitorRef(path.MonitorObject); + auto* monitorContext = GetMonitorContextIfReady(path.MonitorObject); + if (monitorContext != nullptr) + { + monitorContext->ApplyCommittedPath(path.Flags, path.TargetVideoSignalInfo); + } + } return STATUS_SUCCESS; } @@ -4541,6 +4837,17 @@ NTSTATUS VirtualDisplayDriverEvtIddCxMonitorSetGammaRamp( const IDARG_IN_SET_GAMMARAMP* pInArgs ) { + if (pInArgs == nullptr) + { + return STATUS_INVALID_PARAMETER; + } + + IddCxMonitorRefScope monitorRef(MonitorObject); + if (GetMonitorContextIfReady(MonitorObject) == nullptr) + { + return STATUS_INVALID_DEVICE_STATE; + } + stringstream logStream; logStream << "=== PROCESSING GAMMA RAMP REQUEST ==="; vddlog("d", logStream.str().c_str()); diff --git a/Virtual Display Driver (HDR)/MttVDD/Driver.h b/Virtual Display Driver (HDR)/MttVDD/Driver.h index b48b97ff..3863689c 100644 --- a/Virtual Display Driver (HDR)/MttVDD/Driver.h +++ b/Virtual Display Driver (HDR)/MttVDD/Driver.h @@ -49,6 +49,8 @@ namespace Microsoft { namespace IndirectDisp { + class IndirectDeviceContext; + /// /// Manages the creation and lifetime of a Direct3D render device. /// @@ -101,6 +103,39 @@ namespace Microsoft } }; + /// + /// Owns state whose lifetime is tied to one IddCx monitor object. + /// + class IndirectMonitorContext + { + public: + IndirectMonitorContext( + _In_ IndirectDeviceContext* DeviceContext, + _In_ IDDCX_MONITOR Monitor, + _In_ UINT ConnectorIndex); + ~IndirectMonitorContext(); + + IndirectDeviceContext* GetDeviceContext() const; + IDDCX_MONITOR GetMonitor() const; + UINT GetConnectorIndex() const; + + void ApplyCommittedPath( + _In_ IDDCX_PATH_FLAGS Flags, + _In_ const DISPLAYCONFIG_VIDEO_SIGNAL_INFO& TargetSignal); + void ReplaceCursorEvent(_In_opt_ HANDLE CursorEvent); + void ClearCursorEvent(); + + private: + IndirectDeviceContext* m_DeviceContext; + IDDCX_MONITOR m_Monitor; + UINT m_ConnectorIndex; + std::mutex m_StateMutex; + HANDLE m_hCursorEvent; + bool m_PathActive; + bool m_HasCommittedTargetMode; + DISPLAYCONFIG_VIDEO_SIGNAL_INFO m_CommittedTargetSignal; + }; + /// /// Provides a sample implementation of an indirect display driver. /// @@ -115,15 +150,13 @@ namespace Microsoft void CreateMonitor(unsigned int index); - void AssignSwapChain(IDDCX_MONITOR Monitor, IDDCX_SWAPCHAIN SwapChain, LUID RenderAdapter, HANDLE NewFrameEvent); - void UnassignSwapChain(IDDCX_MONITOR Monitor); + void AssignSwapChain(IndirectMonitorContext* MonitorContext, IDDCX_SWAPCHAIN SwapChain, LUID RenderAdapter, HANDLE NewFrameEvent); + void UnassignSwapChain(IndirectMonitorContext* MonitorContext); protected: WDFDEVICE m_WdfDevice; IDDCX_ADAPTER m_Adapter; - IDDCX_MONITOR m_Monitor; - IDDCX_MONITOR m_Monitor2; std::map> m_ProcessingThreads; std::mutex m_ProcessingThreadsMutex; From 82cc313987791b63e18b0c693e472c4aecf1a6eb Mon Sep 17 00:00:00 2001 From: Mike Rodriguez Date: Sat, 19 Sep 2026 00:20:36 -0700 Subject: [PATCH 02/10] ci: restore release-gated SignPath driver signing --- .github/workflows/ci-validation.yml | 119 +++++++++++++++++++++++++++- 1 file changed, 117 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-validation.yml b/.github/workflows/ci-validation.yml index b82179c7..98e3efff 100644 --- a/.github/workflows/ci-validation.yml +++ b/.github/workflows/ci-validation.yml @@ -5,21 +5,35 @@ on: pull_request: push: branches: [ main, master ] + tags: [ 'v*' ] paths-ignore: - '**.md' - 'docs/**' - 'LICENSE*' workflow_dispatch: + inputs: + sign: + description: Submit the built Release packages to SignPath + required: false + default: false + type: boolean concurrency: group: "${{ github.workflow }}-${{ github.ref }}" - cancel-in-progress: true + # A replacement validation build may cancel an older validation build, but a + # submitted signing request must be allowed to complete and download its result. + cancel-in-progress: ${{ !((github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) || (github.event_name == 'workflow_dispatch' && inputs.sign)) }} env: BUILD_CONFIGURATION: Release VDD_SOLUTION: Virtual Display Driver (HDR)/MttVDD.sln + # Signing is deliberate: release tags sign automatically, and a manual run + # must explicitly opt in. Pull requests and ordinary branch pushes never + # receive the SignPath token. + SIGNPATH_SIGNING_RUN: ${{ (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) || (github.event_name == 'workflow_dispatch' && inputs.sign) }} permissions: + actions: read contents: read jobs: @@ -142,9 +156,110 @@ jobs: New-Item -ItemType Directory -Path $dest -Force | Out-Null Copy-Item "$outDir\*" -Destination $dest -Recurse -Force - - name: Upload artifacts + # This is the complete installable UMDF package. SignPath receives the + # GitHub Actions ZIP containing these exact files, with no installer or + # unrelated driver payload mixed in. + $requiredFiles = @("MttVDD.dll", "MttVDD.inf", "MttVDD.cat", "vdd_settings.xml") + foreach ($file in $requiredFiles) { + if (-not (Test-Path (Join-Path $dest $file))) { + throw "Required driver package file not found: $file" + } + } + + - name: Upload unsigned driver package + id: upload_unsigned_driver_package uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: VDD-${{ matrix.platform }}-${{ env.BUILD_CONFIGURATION }} path: artifacts/VDD/${{ matrix.platform }}/ if-no-files-found: error + + - name: Check SignPath configuration + if: env.SIGNPATH_SIGNING_RUN == 'true' + shell: pwsh + env: + SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }} + SIGNPATH_ORG_ID: ${{ vars.SIGNPATH_ORG_ID }} + SIGNPATH_PROJECT_SLUG: ${{ vars.SIGNPATH_PROJECT_SLUG }} + SIGNPATH_POLICY_SLUG: ${{ vars.SIGNPATH_POLICY_SLUG }} + run: | + $ErrorActionPreference = "Stop" + $required = @("SIGNPATH_API_TOKEN", "SIGNPATH_ORG_ID", "SIGNPATH_PROJECT_SLUG", "SIGNPATH_POLICY_SLUG") + $missing = @($required | Where-Object { [string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($_)) }) + if ($missing.Count -gt 0) { + throw "Missing SignPath configuration: $($missing -join ', ')" + } + + # The SignPath project's default artifact configuration must accept the + # GitHub artifact ZIP and preserve MttVDD.dll, MttVDD.inf, MttVDD.cat, + # and vdd_settings.xml. It should Authenticode-sign the DLL and catalog, + # while the INF and XML remain package payload. + - name: Submit VDD package to SignPath + id: submit_signing + if: env.SIGNPATH_SIGNING_RUN == 'true' + uses: signpath/github-action-submit-signing-request@f6d04783b4569d051e0c80105fe66e82819d0092 # v3.0.0 + with: + api-token: ${{ secrets.SIGNPATH_API_TOKEN }} + organization-id: ${{ vars.SIGNPATH_ORG_ID }} + project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }} + signing-policy-slug: ${{ vars.SIGNPATH_POLICY_SLUG }} + github-artifact-id: ${{ steps.upload_unsigned_driver_package.outputs.artifact-id }} + github-token: ${{ github.token }} + wait-for-completion: true + wait-for-completion-timeout-in-seconds: 1800 + output-artifact-directory: signed-artifacts/VDD/${{ matrix.platform }}/ + + - name: Verify signed driver package + if: env.SIGNPATH_SIGNING_RUN == 'true' + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + $signedRoot = "signed-artifacts\VDD\${{ matrix.platform }}" + if (-not (Test-Path $signedRoot)) { throw "SignPath did not create $signedRoot" } + + $driver = @(Get-ChildItem -Path $signedRoot -Recurse -File -Filter "MttVDD.dll") + $catalog = @(Get-ChildItem -Path $signedRoot -Recurse -File -Filter "MttVDD.cat") + $inf = @(Get-ChildItem -Path $signedRoot -Recurse -File -Filter "MttVDD.inf") + $settings = @(Get-ChildItem -Path $signedRoot -Recurse -File -Filter "vdd_settings.xml") + + foreach ($entry in @( + @{ Name = "MttVDD.dll"; Files = $driver }, + @{ Name = "MttVDD.cat"; Files = $catalog }, + @{ Name = "MttVDD.inf"; Files = $inf }, + @{ Name = "vdd_settings.xml"; Files = $settings } + )) { + if ($entry.Files.Count -ne 1) { + throw "Expected exactly one signed package file named $($entry.Name), found $($entry.Files.Count)" + } + } + + $signtool = Get-ChildItem -Path "${env:ProgramFiles(x86)}\Windows Kits\10\bin" -Recurse -File -Filter "signtool.exe" | + Where-Object { $_.FullName -match "\\x64\\signtool.exe$" } | + Sort-Object FullName -Descending | + Select-Object -First 1 + if (-not $signtool) { throw "signtool.exe was not found in the installed Windows SDK" } + + & $signtool.FullName verify /pa /v $driver[0].FullName + if ($LASTEXITCODE -ne 0) { throw "Authenticode verification failed for $($driver[0].FullName)" } + + & $signtool.FullName verify /pa /v $catalog[0].FullName + if ($LASTEXITCODE -ne 0) { throw "Authenticode verification failed for $($catalog[0].FullName)" } + + - name: Upload signed driver package + if: env.SIGNPATH_SIGNING_RUN == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: VDD-${{ matrix.platform }}-${{ env.BUILD_CONFIGURATION }}-signed + path: signed-artifacts/VDD/${{ matrix.platform }}/ + if-no-files-found: error + retention-days: 90 + + - name: Add signing summary + if: env.SIGNPATH_SIGNING_RUN == 'true' + shell: pwsh + env: + SIGNING_REQUEST_URL: ${{ steps.submit_signing.outputs.signing-request-web-url }} + run: | + "## SignPath signing (${{ matrix.platform }})" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append + "- Signed package: `VDD-${{ matrix.platform }}-${{ env.BUILD_CONFIGURATION }}-signed`" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append + "- Signing request: $env:SIGNING_REQUEST_URL" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append From be654b47a803efd07399b8709dfba4e77cb825e7 Mon Sep 17 00:00:00 2001 From: Mike Rodriguez Date: Sat, 19 Sep 2026 01:53:47 -0700 Subject: [PATCH 03/10] fix: avoid racing IddCx path teardown --- .../MttVDD/Driver.cpp | 145 +++--------------- Virtual Display Driver (HDR)/MttVDD/Driver.h | 11 -- 2 files changed, 18 insertions(+), 138 deletions(-) diff --git a/Virtual Display Driver (HDR)/MttVDD/Driver.cpp b/Virtual Display Driver (HDR)/MttVDD/Driver.cpp index a5cc1ae6..093c5577 100644 --- a/Virtual Display Driver (HDR)/MttVDD/Driver.cpp +++ b/Virtual Display Driver (HDR)/MttVDD/Driver.cpp @@ -310,8 +310,11 @@ struct IndirectDeviceContextWrapper void Cleanup() { - delete pContext; + // Publish the unavailable state before destruction so a defensive + // callback check can never observe a pointer while it is being freed. + auto* context = pContext; pContext = nullptr; + delete context; } }; @@ -321,8 +324,9 @@ struct IndirectMonitorContextWrapper void Cleanup() { - delete pContext; + auto* context = pContext; pContext = nullptr; + delete context; } }; void LogQueries(const char* severity, const std::wstring& xmlName) { @@ -1648,27 +1652,6 @@ WDF_DECLARE_CONTEXT_TYPE(IndirectMonitorContextWrapper); namespace { - class IddCxMonitorRefScope - { - public: - explicit IddCxMonitorRefScope(IDDCX_MONITOR monitor) - : m_Object(reinterpret_cast(monitor)) - { - WdfObjectReference(m_Object); - } - - ~IddCxMonitorRefScope() - { - WdfObjectDereference(m_Object); - } - - IddCxMonitorRefScope(const IddCxMonitorRefScope&) = delete; - IddCxMonitorRefScope& operator=(const IddCxMonitorRefScope&) = delete; - - private: - WDFOBJECT m_Object; - }; - IndirectMonitorContext* GetMonitorContextIfReady(IDDCX_MONITOR monitorObject) { auto* wrapper = WdfObjectGet_IndirectMonitorContextWrapper(monitorObject); @@ -3703,21 +3686,15 @@ IndirectMonitorContext::IndirectMonitorContext( _In_ UINT ConnectorIndex) : m_DeviceContext(DeviceContext), m_Monitor(Monitor), - m_ConnectorIndex(ConnectorIndex), - m_hCursorEvent(nullptr), - m_PathActive(false), - m_HasCommittedTargetMode(false), - m_CommittedTargetSignal({}) + m_ConnectorIndex(ConnectorIndex) { } IndirectMonitorContext::~IndirectMonitorContext() { - if (m_DeviceContext != nullptr) - { - m_DeviceContext->UnassignSwapChain(this); - } - ClearCursorEvent(); + // The device context owns and stops all swap-chain processors. WDF can clean + // the parent before its child monitor objects, so do not dereference the raw + // parent pointer from this child cleanup callback. } IndirectDeviceContext* IndirectMonitorContext::GetDeviceContext() const @@ -3735,57 +3712,6 @@ UINT IndirectMonitorContext::GetConnectorIndex() const return m_ConnectorIndex; } -void IndirectMonitorContext::ApplyCommittedPath( - _In_ IDDCX_PATH_FLAGS Flags, - _In_ const DISPLAYCONFIG_VIDEO_SIGNAL_INFO& TargetSignal) -{ - bool shouldUnassign = false; - { - lock_guard lock(m_StateMutex); - if ((Flags & IDDCX_PATH_FLAGS_ACTIVE) == 0) - { - m_PathActive = false; - shouldUnassign = true; - } - else if (TargetSignal.activeSize.cx == 0 || TargetSignal.activeSize.cy == 0) - { - vddlog("w", "Ignoring an active committed path with zero dimensions."); - return; - } - else - { - m_CommittedTargetSignal = TargetSignal; - m_HasCommittedTargetMode = true; - m_PathActive = true; - } - } - - if (shouldUnassign && m_DeviceContext != nullptr) - { - m_DeviceContext->UnassignSwapChain(this); - } -} - -void IndirectMonitorContext::ReplaceCursorEvent(_In_opt_ HANDLE CursorEvent) -{ - HANDLE oldEvent = nullptr; - { - lock_guard lock(m_StateMutex); - oldEvent = m_hCursorEvent; - m_hCursorEvent = CursorEvent; - } - - if (oldEvent != nullptr && oldEvent != INVALID_HANDLE_VALUE) - { - CloseHandle(oldEvent); - } -} - -void IndirectMonitorContext::ClearCursorEvent() -{ - ReplaceCursorEvent(nullptr); -} - IndirectDeviceContext::IndirectDeviceContext(_In_ WDFDEVICE WdfDevice) : m_WdfDevice(WdfDevice), m_Adapter(nullptr) @@ -4045,7 +3971,6 @@ void IndirectDeviceContext::AssignSwapChain(IndirectMonitorContext* MonitorConte } IDDCX_MONITOR Monitor = MonitorContext->GetMonitor(); - MonitorContext->ClearCursorEvent(); // Only cleanup expired devices periodically, not on every assignment static int assignmentCount = 0; @@ -4084,7 +4009,7 @@ void IndirectDeviceContext::AssignSwapChain(IndirectMonitorContext* MonitorConte nullptr, false, false, - nullptr + "VirtualDisplayDriverMouse" ); if (!mouseEvent) @@ -4119,8 +4044,6 @@ void IndirectDeviceContext::AssignSwapChain(IndirectMonitorContext* MonitorConte return; } - MonitorContext->ReplaceCursorEvent(mouseEvent); - vddlog("d", "Hardware cursor setup completed successfully."); } else { @@ -4160,8 +4083,6 @@ void IndirectDeviceContext::UnassignSwapChain(IndirectMonitorContext* MonitorCon { vddlog("w", "UnassignSwapChain called for a monitor without an active processing thread."); } - - MonitorContext->ClearCursorEvent(); } #pragma endregion @@ -4205,22 +4126,11 @@ _Use_decl_annotations_ NTSTATUS VirtualDisplayDriverAdapterCommitModes(IDDCX_ADAPTER AdapterObject, const IDARG_IN_COMMITMODES* pInArgs) { UNREFERENCED_PARAMETER(AdapterObject); + UNREFERENCED_PARAMETER(pInArgs); - if (pInArgs == nullptr || (pInArgs->PathCount != 0 && pInArgs->pPaths == nullptr)) - { - return STATUS_INVALID_PARAMETER; - } - - for (UINT pathIndex = 0; pathIndex < pInArgs->PathCount; ++pathIndex) - { - const IDDCX_PATH& path = pInArgs->pPaths[pathIndex]; - IddCxMonitorRefScope monitorRef(path.MonitorObject); - auto* monitorContext = GetMonitorContextIfReady(path.MonitorObject); - if (monitorContext != nullptr) - { - monitorContext->ApplyCommittedPath(path.Flags, path.TargetVideoSignalInfo); - } - } + // IddCx owns swap-chain lifetime and reports transitions through the + // assign/unassign callbacks. Do not tear down a swap chain from CommitModes; + // doing so races the display pipeline while the path is being committed. return STATUS_SUCCESS; } @@ -4281,7 +4191,6 @@ NTSTATUS VirtualDisplayDriverParseMonitorDescription(const IDARG_IN_PARSEMONITOR _Use_decl_annotations_ NTSTATUS VirtualDisplayDriverMonitorGetDefaultModes(IDDCX_MONITOR MonitorObject, const IDARG_IN_GETDEFAULTDESCRIPTIONMODES* pInArgs, IDARG_OUT_GETDEFAULTDESCRIPTIONMODES* pOutArgs) { - IddCxMonitorRefScope monitorRef(MonitorObject); if (GetMonitorContextIfReady(MonitorObject) == nullptr) { return STATUS_INVALID_DEVICE_STATE; @@ -4388,7 +4297,6 @@ NTSTATUS VirtualDisplayDriverMonitorQueryModes(IDDCX_MONITOR MonitorObject, cons return STATUS_INVALID_PARAMETER; } - IddCxMonitorRefScope monitorRef(MonitorObject); if (GetMonitorContextIfReady(MonitorObject) == nullptr) { return STATUS_INVALID_DEVICE_STATE; @@ -4455,7 +4363,6 @@ NTSTATUS VirtualDisplayDriverMonitorAssignSwapChain(IDDCX_MONITOR MonitorObject, return STATUS_INVALID_PARAMETER; } - IddCxMonitorRefScope monitorRef(MonitorObject); auto* monitorContext = GetMonitorContextIfReady(MonitorObject); if (monitorContext == nullptr || monitorContext->GetDeviceContext() == nullptr) { @@ -4476,7 +4383,6 @@ NTSTATUS VirtualDisplayDriverMonitorAssignSwapChain(IDDCX_MONITOR MonitorObject, _Use_decl_annotations_ NTSTATUS VirtualDisplayDriverMonitorUnassignSwapChain(IDDCX_MONITOR MonitorObject) { - IddCxMonitorRefScope monitorRef(MonitorObject); auto* monitorContext = GetMonitorContextIfReady(MonitorObject); if (monitorContext == nullptr || monitorContext->GetDeviceContext() == nullptr) { @@ -4541,7 +4447,6 @@ NTSTATUS VirtualDisplayDriverEvtIddCxMonitorSetDefaultHdrMetadata( return STATUS_INVALID_PARAMETER; } - IddCxMonitorRefScope monitorRef(MonitorObject); if (GetMonitorContextIfReady(MonitorObject) == nullptr) { return STATUS_INVALID_DEVICE_STATE; @@ -4737,7 +4642,6 @@ NTSTATUS VirtualDisplayDriverEvtIddCxMonitorQueryTargetModes2( return STATUS_INVALID_PARAMETER; } - IddCxMonitorRefScope monitorRef(MonitorObject); if (GetMonitorContextIfReady(MonitorObject) == nullptr) { return STATUS_INVALID_DEVICE_STATE; @@ -4811,22 +4715,10 @@ NTSTATUS VirtualDisplayDriverEvtIddCxAdapterCommitModes2( ) { UNREFERENCED_PARAMETER(AdapterObject); + UNREFERENCED_PARAMETER(pInArgs); - if (pInArgs == nullptr || (pInArgs->PathCount != 0 && pInArgs->pPaths == nullptr)) - { - return STATUS_INVALID_PARAMETER; - } - - for (UINT pathIndex = 0; pathIndex < pInArgs->PathCount; ++pathIndex) - { - const IDDCX_PATH2& path = pInArgs->pPaths[pathIndex]; - IddCxMonitorRefScope monitorRef(path.MonitorObject); - auto* monitorContext = GetMonitorContextIfReady(path.MonitorObject); - if (monitorContext != nullptr) - { - monitorContext->ApplyCommittedPath(path.Flags, path.TargetVideoSignalInfo); - } - } + // Swap-chain lifetime is driven exclusively by IddCx's assign/unassign + // callbacks. CommitModes2 is notification-only for this driver. return STATUS_SUCCESS; } @@ -4842,7 +4734,6 @@ NTSTATUS VirtualDisplayDriverEvtIddCxMonitorSetGammaRamp( return STATUS_INVALID_PARAMETER; } - IddCxMonitorRefScope monitorRef(MonitorObject); if (GetMonitorContextIfReady(MonitorObject) == nullptr) { return STATUS_INVALID_DEVICE_STATE; diff --git a/Virtual Display Driver (HDR)/MttVDD/Driver.h b/Virtual Display Driver (HDR)/MttVDD/Driver.h index 3863689c..89040d24 100644 --- a/Virtual Display Driver (HDR)/MttVDD/Driver.h +++ b/Virtual Display Driver (HDR)/MttVDD/Driver.h @@ -119,21 +119,10 @@ namespace Microsoft IDDCX_MONITOR GetMonitor() const; UINT GetConnectorIndex() const; - void ApplyCommittedPath( - _In_ IDDCX_PATH_FLAGS Flags, - _In_ const DISPLAYCONFIG_VIDEO_SIGNAL_INFO& TargetSignal); - void ReplaceCursorEvent(_In_opt_ HANDLE CursorEvent); - void ClearCursorEvent(); - private: IndirectDeviceContext* m_DeviceContext; IDDCX_MONITOR m_Monitor; UINT m_ConnectorIndex; - std::mutex m_StateMutex; - HANDLE m_hCursorEvent; - bool m_PathActive; - bool m_HasCommittedTargetMode; - DISPLAYCONFIG_VIDEO_SIGNAL_INFO m_CommittedTargetSignal; }; /// From 29f139fef5d975b5b1b25cb8789c0fb576c473cd Mon Sep 17 00:00:00 2001 From: Mike Rodriguez Date: Sat, 19 Sep 2026 02:10:56 -0700 Subject: [PATCH 04/10] build: use calendar driver versions --- .github/workflows/ci-validation.yml | 11 +++++++++++ .../MttVDD/MttVDD.vcxproj | 16 +++++++++++----- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-validation.yml b/.github/workflows/ci-validation.yml index 98e3efff..0899b566 100644 --- a/.github/workflows/ci-validation.yml +++ b/.github/workflows/ci-validation.yml @@ -133,6 +133,15 @@ jobs: $umdfMinor = ($umdfBest.Name -split '\.')[1] Write-Output "Using UMDF version: $($umdfBest.Name) (minor=$umdfMinor) from $($umdfBest.FullName)" + $now = [DateTime]::UtcNow + $buildNumber = [int]$env:GITHUB_RUN_NUMBER + if ($buildNumber -lt 0 -or $buildNumber -gt 65535) { + throw "GITHUB_RUN_NUMBER must fit an INF version component (0-65535): $buildNumber" + } + $driverVersion = "{0}.{1}.{2}.{3}" -f ($now.Year % 100), $now.Month, $now.Day, $buildNumber + $driverDate = $now.ToString("MM/dd/yyyy", [Globalization.CultureInfo]::InvariantCulture) + Write-Output "Stamping DriverVer=$driverDate,$driverVersion" + msbuild $sln ` /m ` /t:Build ` @@ -141,6 +150,8 @@ jobs: /p:WindowsSdkDir="$env:WINDOWS_SDK_DIR" ` /p:WindowsTargetPlatformVersion="$env:WINDOWS_TARGET_PLATFORM_VERSION" ` /p:UMDF_VERSION_MINOR="$umdfMinor" ` + /p:VddDriverVersion="$driverVersion" ` + /p:VddDriverDate="$driverDate" ` /p:EnableInfVerif=false ` /p:RunApiValidator=false ` /verbosity:minimal diff --git a/Virtual Display Driver (HDR)/MttVDD/MttVDD.vcxproj b/Virtual Display Driver (HDR)/MttVDD/MttVDD.vcxproj index 6d20ef7f..833241bb 100644 --- a/Virtual Display Driver (HDR)/MttVDD/MttVDD.vcxproj +++ b/Virtual Display Driver (HDR)/MttVDD/MttVDD.vcxproj @@ -175,6 +175,12 @@ + + + $([System.DateTime]::UtcNow.ToString('yy.M.d')).1 + $([System.DateTime]::UtcNow.ToString('MM/dd/yyyy')) + DbgengRemoteDebugger true @@ -324,9 +330,6 @@ copy "$(ProjectDir)..\vdd_settings.xml" "$(TargetDir)\MttVDD" - - 10/16/2024 - @@ -348,8 +351,11 @@ copy "$(ProjectDir)..\vdd_settings.xml" "$(TargetDir)\MttVDD" + + - 10/16/2024 + $(VddDriverDate) + $(VddDriverVersion) @@ -364,4 +370,4 @@ - \ No newline at end of file + From 3f84d49175940f0c575a5bd33bfacb6f240455bf Mon Sep 17 00:00:00 2001 From: Mike Rodriguez Date: Sat, 19 Sep 2026 02:35:31 -0700 Subject: [PATCH 05/10] ci: select VDD SignPath artifact configuration --- .github/workflows/ci-validation.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci-validation.yml b/.github/workflows/ci-validation.yml index 0899b566..ae2a2aa0 100644 --- a/.github/workflows/ci-validation.yml +++ b/.github/workflows/ci-validation.yml @@ -214,6 +214,7 @@ jobs: organization-id: ${{ vars.SIGNPATH_ORG_ID }} project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }} signing-policy-slug: ${{ vars.SIGNPATH_POLICY_SLUG }} + artifact-configuration-slug: VDD_driver_package github-artifact-id: ${{ steps.upload_unsigned_driver_package.outputs.artifact-id }} github-token: ${{ github.token }} wait-for-completion: true From 0ebca45f75b1547f1f6df966a4b0d35685677693 Mon Sep 17 00:00:00 2001 From: Mike Rodriguez Date: Sat, 19 Sep 2026 03:52:14 -0700 Subject: [PATCH 06/10] build: bundle and sign Virtual Driver Control --- .github/workflows/ci-validation.yml | 100 +- README.md | 4 + VirtualDriverControl/.gitignore | 5 + VirtualDriverControl/README.md | 77 + VirtualDriverControl/electron.vite.config.ts | 39 + VirtualDriverControl/package-lock.json | 6167 +++++++++++++++++ VirtualDriverControl/package.json | 66 + VirtualDriverControl/resources/VDD_Red.ico | Bin 0 -> 270398 bytes VirtualDriverControl/resources/VDD_Yellow.ico | Bin 0 -> 270398 bytes .../resources/VirtualDisplayDriver.ico | Bin 0 -> 270398 bytes .../scripts/render-audio-interop.mjs | 19 + .../scripts/render-ps-scripts.mjs | 81 + VirtualDriverControl/src/main/index.ts | 238 + VirtualDriverControl/src/main/ipc.ts | 243 + .../src/main/services/audio-service.ts | 309 + .../src/main/services/display-service.ts | 116 + .../src/main/services/driver-service.ts | 254 + .../src/main/services/installer-service.ts | 716 ++ .../src/main/services/log-service.ts | 142 + .../src/main/services/pipe-client.ts | 308 + .../src/main/services/prefs-service.ts | 167 + .../src/main/services/settings-service.ts | 516 ++ VirtualDriverControl/src/preload/index.d.ts | 9 + VirtualDriverControl/src/preload/index.ts | 113 + VirtualDriverControl/src/renderer/index.html | 16 + VirtualDriverControl/src/renderer/src/App.tsx | 72 + .../src/renderer/src/assets/logo-err.png | Bin 0 -> 24009 bytes .../src/renderer/src/assets/logo-ok.png | Bin 0 -> 34800 bytes .../src/renderer/src/assets/logo-warn.png | Bin 0 -> 31041 bytes .../src/components/ArrangementMap.tsx | 184 + .../renderer/src/components/CieDiagram.tsx | 202 + .../src/components/DriverLifecycle.tsx | 250 + .../src/renderer/src/components/SaveBar.tsx | 47 + .../src/renderer/src/components/Sidebar.tsx | 66 + .../src/renderer/src/components/TitleBar.tsx | 64 + .../src/renderer/src/components/Toasts.tsx | 42 + .../src/renderer/src/components/ui.tsx | 185 + .../src/renderer/src/env.d.ts | 9 + .../src/renderer/src/main.tsx | 10 + .../src/renderer/src/pages/AudioPage.tsx | 284 + .../src/renderer/src/pages/ColorPage.tsx | 264 + .../src/renderer/src/pages/ConsolePage.tsx | 187 + .../src/renderer/src/pages/DashboardPage.tsx | 184 + .../src/renderer/src/pages/DisplaysPage.tsx | 336 + .../src/renderer/src/pages/EdidPage.tsx | 336 + .../src/renderer/src/pages/GpuPage.tsx | 149 + .../src/renderer/src/pages/SettingsPage.tsx | 400 ++ .../src/renderer/src/stores/audio.ts | 220 + .../src/renderer/src/stores/driver.ts | 205 + .../src/renderer/src/stores/installer.ts | 194 + .../src/renderer/src/stores/logs.ts | 66 + .../src/renderer/src/stores/settings.ts | 84 + .../src/renderer/src/stores/ui.ts | 86 + .../src/renderer/src/styles/global.css | 1936 ++++++ .../src/renderer/src/utils/audio-router.ts | 126 + .../src/renderer/src/utils/diff.ts | 66 + VirtualDriverControl/src/shared/defaults.ts | 109 + VirtualDriverControl/src/shared/edid.ts | 445 ++ VirtualDriverControl/src/shared/presets.ts | 65 + VirtualDriverControl/src/shared/types.ts | 419 ++ VirtualDriverControl/tsconfig.json | 7 + VirtualDriverControl/tsconfig.node.json | 26 + VirtualDriverControl/tsconfig.web.json | 27 + 63 files changed, 17045 insertions(+), 12 deletions(-) create mode 100644 VirtualDriverControl/.gitignore create mode 100644 VirtualDriverControl/README.md create mode 100644 VirtualDriverControl/electron.vite.config.ts create mode 100644 VirtualDriverControl/package-lock.json create mode 100644 VirtualDriverControl/package.json create mode 100644 VirtualDriverControl/resources/VDD_Red.ico create mode 100644 VirtualDriverControl/resources/VDD_Yellow.ico create mode 100644 VirtualDriverControl/resources/VirtualDisplayDriver.ico create mode 100644 VirtualDriverControl/scripts/render-audio-interop.mjs create mode 100644 VirtualDriverControl/scripts/render-ps-scripts.mjs create mode 100644 VirtualDriverControl/src/main/index.ts create mode 100644 VirtualDriverControl/src/main/ipc.ts create mode 100644 VirtualDriverControl/src/main/services/audio-service.ts create mode 100644 VirtualDriverControl/src/main/services/display-service.ts create mode 100644 VirtualDriverControl/src/main/services/driver-service.ts create mode 100644 VirtualDriverControl/src/main/services/installer-service.ts create mode 100644 VirtualDriverControl/src/main/services/log-service.ts create mode 100644 VirtualDriverControl/src/main/services/pipe-client.ts create mode 100644 VirtualDriverControl/src/main/services/prefs-service.ts create mode 100644 VirtualDriverControl/src/main/services/settings-service.ts create mode 100644 VirtualDriverControl/src/preload/index.d.ts create mode 100644 VirtualDriverControl/src/preload/index.ts create mode 100644 VirtualDriverControl/src/renderer/index.html create mode 100644 VirtualDriverControl/src/renderer/src/App.tsx create mode 100644 VirtualDriverControl/src/renderer/src/assets/logo-err.png create mode 100644 VirtualDriverControl/src/renderer/src/assets/logo-ok.png create mode 100644 VirtualDriverControl/src/renderer/src/assets/logo-warn.png create mode 100644 VirtualDriverControl/src/renderer/src/components/ArrangementMap.tsx create mode 100644 VirtualDriverControl/src/renderer/src/components/CieDiagram.tsx create mode 100644 VirtualDriverControl/src/renderer/src/components/DriverLifecycle.tsx create mode 100644 VirtualDriverControl/src/renderer/src/components/SaveBar.tsx create mode 100644 VirtualDriverControl/src/renderer/src/components/Sidebar.tsx create mode 100644 VirtualDriverControl/src/renderer/src/components/TitleBar.tsx create mode 100644 VirtualDriverControl/src/renderer/src/components/Toasts.tsx create mode 100644 VirtualDriverControl/src/renderer/src/components/ui.tsx create mode 100644 VirtualDriverControl/src/renderer/src/env.d.ts create mode 100644 VirtualDriverControl/src/renderer/src/main.tsx create mode 100644 VirtualDriverControl/src/renderer/src/pages/AudioPage.tsx create mode 100644 VirtualDriverControl/src/renderer/src/pages/ColorPage.tsx create mode 100644 VirtualDriverControl/src/renderer/src/pages/ConsolePage.tsx create mode 100644 VirtualDriverControl/src/renderer/src/pages/DashboardPage.tsx create mode 100644 VirtualDriverControl/src/renderer/src/pages/DisplaysPage.tsx create mode 100644 VirtualDriverControl/src/renderer/src/pages/EdidPage.tsx create mode 100644 VirtualDriverControl/src/renderer/src/pages/GpuPage.tsx create mode 100644 VirtualDriverControl/src/renderer/src/pages/SettingsPage.tsx create mode 100644 VirtualDriverControl/src/renderer/src/stores/audio.ts create mode 100644 VirtualDriverControl/src/renderer/src/stores/driver.ts create mode 100644 VirtualDriverControl/src/renderer/src/stores/installer.ts create mode 100644 VirtualDriverControl/src/renderer/src/stores/logs.ts create mode 100644 VirtualDriverControl/src/renderer/src/stores/settings.ts create mode 100644 VirtualDriverControl/src/renderer/src/stores/ui.ts create mode 100644 VirtualDriverControl/src/renderer/src/styles/global.css create mode 100644 VirtualDriverControl/src/renderer/src/utils/audio-router.ts create mode 100644 VirtualDriverControl/src/renderer/src/utils/diff.ts create mode 100644 VirtualDriverControl/src/shared/defaults.ts create mode 100644 VirtualDriverControl/src/shared/edid.ts create mode 100644 VirtualDriverControl/src/shared/presets.ts create mode 100644 VirtualDriverControl/src/shared/types.ts create mode 100644 VirtualDriverControl/tsconfig.json create mode 100644 VirtualDriverControl/tsconfig.node.json create mode 100644 VirtualDriverControl/tsconfig.web.json diff --git a/.github/workflows/ci-validation.yml b/.github/workflows/ci-validation.yml index ae2a2aa0..5071a279 100644 --- a/.github/workflows/ci-validation.yml +++ b/.github/workflows/ci-validation.yml @@ -27,6 +27,7 @@ concurrency: env: BUILD_CONFIGURATION: Release VDD_SOLUTION: Virtual Display Driver (HDR)/MttVDD.sln + CONTROL_PANEL_DIR: VirtualDriverControl # Signing is deliberate: release tags sign automatically, and a manual run # must explicitly opt in. Pull requests and ordinary branch pushes never # receive the SignPath token. @@ -51,6 +52,20 @@ jobs: with: submodules: true + - name: Compute release version + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + $now = [DateTime]::UtcNow + $buildNumber = [int]$env:GITHUB_RUN_NUMBER + if ($buildNumber -lt 0 -or $buildNumber -gt 65535) { + throw "GITHUB_RUN_NUMBER must fit an INF version component (0-65535): $buildNumber" + } + + $releaseVersion = "{0}.{1}.{2}.{3}" -f ($now.Year % 100), $now.Month, $now.Day, $buildNumber + Write-Output "Using release version $releaseVersion" + "RELEASE_VERSION=$releaseVersion" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + - name: Setup MSBuild uses: microsoft/setup-msbuild@30375c66a4eea26614e0d39710365f22f8b0af57 # v3.0.0 @@ -134,11 +149,7 @@ jobs: Write-Output "Using UMDF version: $($umdfBest.Name) (minor=$umdfMinor) from $($umdfBest.FullName)" $now = [DateTime]::UtcNow - $buildNumber = [int]$env:GITHUB_RUN_NUMBER - if ($buildNumber -lt 0 -or $buildNumber -gt 65535) { - throw "GITHUB_RUN_NUMBER must fit an INF version component (0-65535): $buildNumber" - } - $driverVersion = "{0}.{1}.{2}.{3}" -f ($now.Year % 100), $now.Month, $now.Day, $buildNumber + $driverVersion = $env:RELEASE_VERSION $driverDate = $now.ToString("MM/dd/yyyy", [Globalization.CultureInfo]::InvariantCulture) Write-Output "Stamping DriverVer=$driverDate,$driverVersion" @@ -156,6 +167,58 @@ jobs: /p:RunApiValidator=false ` /verbosity:minimal + - name: Setup Node.js + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: 24 + cache: npm + cache-dependency-path: VirtualDriverControl/package-lock.json + + - name: Build Control Panel (${{ matrix.platform }}) + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + $controlArch = if ("${{ matrix.platform }}" -eq "ARM64") { "arm64" } else { "x64" } + + Push-Location "${{ env.CONTROL_PANEL_DIR }}" + try { + npm ci + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + # Fail releases for moderate-or-higher dependency advisories. The + # remaining low advisory is isolated to the local Vite dev server + # and is not included in the packaged application. + npm audit --audit-level=moderate + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + npm version $env:RELEASE_VERSION --no-git-tag-version + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + npm run typecheck + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + npm run test:generated + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + $parseErrors = @() + Get-ChildItem "out-ps-check\*.ps1" | ForEach-Object { + $tokens = $null + $errors = $null + [void][System.Management.Automation.Language.Parser]::ParseFile($_.FullName, [ref]$tokens, [ref]$errors) + $parseErrors += $errors + } + if ($parseErrors.Count -gt 0) { + $parseErrors | Format-List + throw "Generated installer PowerShell failed to parse" + } + Add-Type -Path (Resolve-Path "out-ps-check\core-audio-interop.cs") + + npm run "build-portable:$controlArch" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + } finally { + Pop-Location + } + - name: Collect outputs shell: pwsh run: | @@ -167,10 +230,17 @@ jobs: New-Item -ItemType Directory -Path $dest -Force | Out-Null Copy-Item "$outDir\*" -Destination $dest -Recurse -Force - # This is the complete installable UMDF package. SignPath receives the - # GitHub Actions ZIP containing these exact files, with no installer or - # unrelated driver payload mixed in. - $requiredFiles = @("MttVDD.dll", "MttVDD.inf", "MttVDD.cat", "vdd_settings.xml") + $controlArch = if ("${{ matrix.platform }}" -eq "ARM64") { "arm64" } else { "x64" } + $controlSource = "${{ env.CONTROL_PANEL_DIR }}\dist\Virtual Driver Control-$env:RELEASE_VERSION-$controlArch.exe" + if (-not (Test-Path $controlSource)) { + throw "Control Panel executable not found: $controlSource" + } + Copy-Item $controlSource -Destination (Join-Path $dest "Virtual Driver Control.exe") -Force + + # This is the complete installable UMDF package and its matching + # Control Panel. SignPath receives the GitHub Actions ZIP containing + # these exact files, with no stale driver payload mixed in. + $requiredFiles = @("MttVDD.dll", "MttVDD.inf", "MttVDD.cat", "vdd_settings.xml", "Virtual Driver Control.exe") foreach ($file in $requiredFiles) { if (-not (Test-Path (Join-Path $dest $file))) { throw "Required driver package file not found: $file" @@ -203,8 +273,9 @@ jobs: # The SignPath project's default artifact configuration must accept the # GitHub artifact ZIP and preserve MttVDD.dll, MttVDD.inf, MttVDD.cat, - # and vdd_settings.xml. It should Authenticode-sign the DLL and catalog, - # while the INF and XML remain package payload. + # vdd_settings.xml, and Virtual Driver Control.exe. It should + # Authenticode-sign the DLL, catalog, and Control Panel executable while + # the INF and XML remain package payload. - name: Submit VDD package to SignPath id: submit_signing if: env.SIGNPATH_SIGNING_RUN == 'true' @@ -233,12 +304,14 @@ jobs: $catalog = @(Get-ChildItem -Path $signedRoot -Recurse -File -Filter "MttVDD.cat") $inf = @(Get-ChildItem -Path $signedRoot -Recurse -File -Filter "MttVDD.inf") $settings = @(Get-ChildItem -Path $signedRoot -Recurse -File -Filter "vdd_settings.xml") + $controlPanel = @(Get-ChildItem -Path $signedRoot -Recurse -File -Filter "Virtual Driver Control.exe") foreach ($entry in @( @{ Name = "MttVDD.dll"; Files = $driver }, @{ Name = "MttVDD.cat"; Files = $catalog }, @{ Name = "MttVDD.inf"; Files = $inf }, - @{ Name = "vdd_settings.xml"; Files = $settings } + @{ Name = "vdd_settings.xml"; Files = $settings }, + @{ Name = "Virtual Driver Control.exe"; Files = $controlPanel } )) { if ($entry.Files.Count -ne 1) { throw "Expected exactly one signed package file named $($entry.Name), found $($entry.Files.Count)" @@ -257,6 +330,9 @@ jobs: & $signtool.FullName verify /pa /v $catalog[0].FullName if ($LASTEXITCODE -ne 0) { throw "Authenticode verification failed for $($catalog[0].FullName)" } + & $signtool.FullName verify /pa /v $controlPanel[0].FullName + if ($LASTEXITCODE -ne 0) { throw "Authenticode verification failed for $($controlPanel[0].FullName)" } + - name: Upload signed driver package if: env.SIGNPATH_SIGNING_RUN == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/README.md b/README.md index 83106c24..3dcc9e38 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,10 @@ For an existing checkout, initialize the submodule before building: git submodule update --init --recursive ``` +The release workflow also builds the Control Panel in [`VirtualDriverControl`](VirtualDriverControl/README.md). +Its portable executable is versioned with the same `YY.M.D.GITHUB_RUN_NUMBER` value as the driver, packaged beside +the matching x64 or ARM64 driver, and submitted with the driver DLL and catalog for code signing. + ## ⬇️ Download Latest Version - [Driver Installer (Windows 10/11)](https://github.com/VirtualDrivers/Virtual-Display-Driver/releases) - Check the [Releases](https://github.com/VirtualDrivers/Virtual-Display-Driver/releases) page for the latest version and release notes. diff --git a/VirtualDriverControl/.gitignore b/VirtualDriverControl/.gitignore new file mode 100644 index 00000000..2bcdeb0c --- /dev/null +++ b/VirtualDriverControl/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +out/ +out-ps-check/ +*.tsbuildinfo diff --git a/VirtualDriverControl/README.md b/VirtualDriverControl/README.md new file mode 100644 index 00000000..41c5db70 --- /dev/null +++ b/VirtualDriverControl/README.md @@ -0,0 +1,77 @@ +# Virtual Driver Control + +A modern control panel for the [Virtual Display Driver](https://github.com/VirtualDrivers/Virtual-Display-Driver) (MttVDD). +Electron + React + TypeScript, talking to the driver over its native named pipe. + +## What it does + +- **Dashboard** — live driver status (PING heartbeat), an animated monitor stage, and one-click add/remove of + virtual displays via `SETDISPLAYCOUNT`. Quick toggles for HDR+, SDR 10-bit, hardware cursor, custom EDID and logging + apply instantly through the pipe. +- **Driver lifecycle** — download & install the latest signed [Virtual Display Driver](https://github.com/VirtualDrivers/Virtual-Display-Driver/releases) + and [Virtual Audio Driver](https://github.com/VirtualDrivers/Virtual-Audio-Driver/releases) releases: checksum-verified + download, signer trust, `pnputil` staging and SetupAPI device-node creation, plus restart-device and uninstall — + one UAC prompt per operation. The audio driver supports **multiple instances** (1-4 independent speaker + mic pairs). +- **Audio** — Windows endpoint control (volume, mute, default device for every playback/recording endpoint via Core + Audio) and a live **routing matrix**: pump any microphone or the system output mix into any output device with + per-route gain and level meters. Routes persist and re-arm on launch. Covers mic → speaker, speaker → speaker + (System audio source) and speaker → mic (route into the virtual speaker; apps hear it on the virtual microphone). +- **Displays** — a live, to-scale **desktop arrangement map** of every attached monitor (physical and virtual, with + real placement, resolution, refresh rate and scale - virtual displays are flagged via PnP parent lookup), plus a + full editor for `vdd_settings.xml` resolutions: preset gallery (VGA → 8K, ultrawides, tablets), per-resolution and + global refresh rates (fractional rates supported), preferred/fallback mode, and a to-scale size comparison. +- **HDR & Color** — color format (RGB / YCbCr), HDR10 static metadata, gamma and color space, plus an interactive + CIE 1931 chromaticity diagram with draggable R/G/B/white points. +- **EDID Lab** — drag-and-drop EDID decoder (pure TypeScript: identity, timings, chromaticity, CEA-861, HDR metadata). + One click exports an IddCx `monitor_profile.xml` + `user_edid.bin` and enables EDID integration. +- **GPU** — adapter list from the driver (`GETALLGPUS`) with WMI fallback, one-click `SETGPU` assignment. +- **Console** — unified live feed: driver file logs (tailed), every pipe command's streamed response, and app events. + Filterable by severity/source, with a raw command input for power users. +- **Settings** — cursor, logging and auto-resolution options, a line-diff preview before every save, automatic + timestamped backups with restore, themes (dark/light/system) and accent colors. + +## Driver integration + +| Mechanism | Use | +| --- | --- | +| `\\.\pipe\MTTVirtualDisplayPipe` | Live control. Commands are sent UTF-16LE on one-shot connections; responses are read until disconnect. All calls are serialized with timeouts and a reload cooldown. | +| `C:\VirtualDisplayDriver\vdd_settings.xml` | Full typed read/write of every section, with atomic writes and automatic backups. | +| `C:\VirtualDisplayDriver\Logs\` | Daily log files are tailed into the console feed. | +| PowerShell / WMI | Fallbacks for driver presence and GPU enumeration when the pipe is down. | +| GitHub releases API | Driver lifecycle for both drivers: fetches the latest driver package (x64/ARM64 picked automatically), verifies its published SHA-256, then installs through an elevated PowerShell script (signer → TrustedPublisher, `pnputil /add-driver`, SetupAPI root-device creation). Uninstall removes the devices and driver package but keeps your configuration. | +| Core Audio (COM interop) | Audio endpoint enumeration, volume/mute (`IAudioEndpointVolume`) and default-device switching (`IPolicyConfig`) - no elevation needed. | +| WebAudio + WASAPI loopback | The in-app routing engine: capture any input device or the system mix and play it to any output. Routes are active while the app runs. | +| Electron `screen` API | Desktop arrangement map with real bounds, scale, rotation and refresh rate for every display. | + +`RELOAD_DRIVER` is intentionally **never sent** (upstream undefined behavior). Saving applies changes by writing the +XML and issuing `SETDISPLAYCOUNT `, which makes the driver reload its configuration safely. + +The app runs fully offline as well: with no driver installed you can still edit, preview and save configuration. + +## Development + +```bash +npm install +npm run dev # hot-reloading dev session +npm run typecheck # strict TS for main + preload + renderer +npm run test:generated # render embedded PowerShell/C# for syntax checks +npm run build # production bundles into out/ +npm run build-portable # portable .exe via electron-builder (admin elevation) +npm run build-portable:x64 # architecture-specific release executable +npm run build-portable:arm64 # architecture-specific release executable +``` + +Requires Windows and Node 18+. Run elevated if you want to write to `C:\VirtualDisplayDriver`. + +## Architecture + +``` +src/ + main/ Electron main process: PipeClient, SettingsService, DriverService, LogService, IPC + preload/ contextBridge API (window.vdd) - the only door between renderer and system + renderer/ React UI: pages, components, zustand stores, design tokens + shared/ Types, vdd_settings schema defaults, EDID parser - imported by all processes +``` + +Security: `contextIsolation` + `sandbox` enabled, no `nodeIntegration`, every IPC input validated in the main process, +raw pipe commands restricted to a conservative charset, external links limited to https. diff --git a/VirtualDriverControl/electron.vite.config.ts b/VirtualDriverControl/electron.vite.config.ts new file mode 100644 index 00000000..efe70543 --- /dev/null +++ b/VirtualDriverControl/electron.vite.config.ts @@ -0,0 +1,39 @@ +import { defineConfig } from 'electron-vite' +import react from '@vitejs/plugin-react' +import { resolve } from 'path' + +export default defineConfig({ + main: { + resolve: { + alias: { + '@shared': resolve(__dirname, 'src/shared') + } + }, + build: { + rollupOptions: { + output: { format: 'cjs' } + } + } + }, + preload: { + resolve: { + alias: { + '@shared': resolve(__dirname, 'src/shared') + } + }, + build: { + rollupOptions: { + output: { format: 'cjs' } + } + } + }, + renderer: { + resolve: { + alias: { + '@renderer': resolve(__dirname, 'src/renderer/src'), + '@shared': resolve(__dirname, 'src/shared') + } + }, + plugins: [react()] + } +}) diff --git a/VirtualDriverControl/package-lock.json b/VirtualDriverControl/package-lock.json new file mode 100644 index 00000000..36b664fc --- /dev/null +++ b/VirtualDriverControl/package-lock.json @@ -0,0 +1,6167 @@ +{ + "name": "virtual-driver-control", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "virtual-driver-control", + "version": "2.0.0", + "license": "MIT", + "devDependencies": { + "@types/node": "^25.9.3", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.2.0", + "electron": "^42.4.0", + "electron-builder": "^26.15.2", + "electron-vite": "^5.0.0", + "fast-xml-parser": "^5.8.0", + "lucide-react": "^1.17.0", + "motion": "^12.40.0", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "typescript": "^6.0.3", + "vite": "^7.3.5", + "zustand": "^5.0.14" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@electron-internal/extract-zip": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.2.tgz", + "integrity": "sha512-VJuNETNPEhrmQEZezeTZO5TZMV+dobBRyJ7zHjGJWIhMS7m7W1UeClt69u4hkUxv9ZZVxuli/E9Yvc4gDNHGsg==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/get": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-5.0.0.tgz", + "integrity": "sha512-pjoBpru1KdEtcExBnuHAP1cAc/5faoedw0hzJkL3o4/IJp7HNF1+fbrdxT3gMYRX2oJfvnA/WXeCTVQpYYxyJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^3.0.0", + "graceful-fs": "^4.2.11", + "progress": "^2.0.3", + "semver": "^7.6.3", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=22.12.0" + }, + "optionalDependencies": { + "undici": "^7.24.4" + } + }, + "node_modules/@electron/get/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", + "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/rebuild": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.4.tgz", + "integrity": "sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.1.1", + "node-abi": "^4.2.0", + "node-api-version": "^0.2.1", + "node-gyp": "^12.2.0", + "read-binary-file-arch": "^1.0.6" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", + "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.3.1", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodable/entities": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.1.tgz", + "integrity": "sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.7.0.tgz", + "integrity": "sha512-W8ZfWzLmQnrcky+eh3tni4IozMdqBDiHWU0N+vve/UGjMaUs8c0L7A2oEdkBXS8rTpWDpK/aoI3DG/L/hxmxPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/json-schema": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", + "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@peculiar/utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", + "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/webcrypto": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.7.1.tgz", + "integrity": "sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/json-schema": "^1.1.12", + "@peculiar/utils": "^2.0.2", + "tslib": "^2.8.1", + "webcrypto-core": "^1.9.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", + "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", + "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", + "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", + "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", + "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", + "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", + "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", + "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", + "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", + "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", + "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", + "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", + "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", + "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", + "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", + "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", + "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", + "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", + "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", + "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", + "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", + "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", + "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", + "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", + "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz", + "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.15.tgz", + "integrity": "sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anynum": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.0.tgz", + "integrity": "sha512-xjR9/zBVnUOP6ztMIIgShjsxui80nQUQH+5xJnvrYLs+90bF25/KJqaAi8mk+B4RDtX1Nspi6fmp4YTEts8SfA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/app-builder-lib": { + "version": "26.15.2", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.15.2.tgz", + "integrity": "sha512-3mYfKOjr/ZY7gFESOcq8kylBMgGPpmlQYnpBVit4p6zIg0t/8bkWBILdMMtnjFyN2jllyBf225T8dLlz3D6oBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "3.4.1", + "@electron/fuses": "^1.8.0", + "@electron/get": "^3.0.0", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.3", + "@electron/rebuild": "^4.0.4", + "@electron/universal": "2.0.3", + "@malept/flatpak-bundler": "^0.4.0", + "@noble/hashes": "^2.2.0", + "@peculiar/webcrypto": "^1.7.1", + "@types/fs-extra": "9.0.13", + "ajv": "^8.18.0", + "asn1js": "^3.0.10", + "async-exit-hook": "^2.0.1", + "builder-util": "26.15.0", + "builder-util-runtime": "9.7.0", + "chromium-pickle-js": "^0.2.0", + "ci-info": "4.3.1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "26.15.1", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "isbinaryfile": "^5.0.0", + "jiti": "^2.4.2", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.2.5", + "pkijs": "^3.4.0", + "plist": "3.1.0", + "proper-lockfile": "^4.1.2", + "resedit": "^1.7.0", + "semver": "~7.7.3", + "tar": "^7.5.7", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0", + "unzipper": "^0.12.3", + "which": "^5.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "26.15.2", + "electron-builder-squirrel-windows": "26.15.2" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/app-builder-lib/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/app-builder-lib/node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/app-builder-lib/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asn1js": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.5", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.25", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.25.tgz", + "integrity": "sha512-gMmEShwwq7FJqMwvfRwvCl00v4kN+KOfJqXn+f4nrufak5gNHJOksd/60Dvjuz7sI8Y5WiSFBa8FEYr+zoyqCw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.0.tgz", + "integrity": "sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.23", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.427", + "node-releases": "^2.0.55", + "update-browserslist-db": "^1.3.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "26.15.0", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.15.0.tgz", + "integrity": "sha512-dUx+HxVbiNsNQ4mGe1PyoC/tBmsHwBNDLdBuqWCj+rhHFE9lHgrXiGYKAM1uNlznhAaUSyMlms84VeSSr3gOBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.6", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "js-yaml": "^4.1.0", + "sanitize-filename": "^1.6.3", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz", + "integrity": "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-compare/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dmg-builder": { + "version": "26.15.2", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.15.2.tgz", + "integrity": "sha512-fMkjRqKyPtsz4Kzu/qGP0BGjqzMCIgp+/7kw/u6YH6lvn/8hvL3c0TXhoFayBoYdpPCnEinnCHztd4bW7/jetA==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.15.2", + "builder-util": "26.15.0", + "fs-extra": "^10.1.0", + "js-yaml": "^4.1.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "42.11.6", + "resolved": "https://registry.npmjs.org/electron/-/electron-42.11.6.tgz", + "integrity": "sha512-IgDsjQp+CYLnArfQbzzS2VmkRgHmSU1G9nGVOtSnFpkUYR8rkeAUSDO77qtqAPX1jgAGly1kgisUmwB9CbUHXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-internal/extract-zip": "^1.0.1", + "@electron/get": "^5.0.0", + "@types/node": "^24.9.0" + }, + "bin": { + "electron": "cli.js", + "install-electron": "install.js" + }, + "engines": { + "node": ">= 22.12.0" + } + }, + "node_modules/electron-builder": { + "version": "26.15.2", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.15.2.tgz", + "integrity": "sha512-veKM9+dCljaC5A74Pwc0ZWQ9arOHREXWh9hUIf8NGg49ch7x+IB4QhbMzIrV5ONZIXM2OEkaxW11cAPjPtoi4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.15.2", + "builder-util": "26.15.0", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "dmg-builder": "26.15.2", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "26.15.2", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.15.2.tgz", + "integrity": "sha512-PNl+SSRoma9mXhxycNGxutZPgvmK19v41mn8F9oecpAU2QNAldpB4HfMuA1LwFC2j8aRzzV5M9HKlKe6dfpvNw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.15.2", + "builder-util": "26.15.0", + "electron-winstaller": "5.4.0" + } + }, + "node_modules/electron-publish": { + "version": "26.15.1", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.15.1.tgz", + "integrity": "sha512-BMgMHOyexWn0UnOC+Afffw0DMrr0yfLp4U8YsLXwoJ3Da7LS7WUnz21teYZqO0gaApE1KgsjREWmbPqvF5JcPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "aws4": "^1.13.2", + "builder-util": "26.15.0", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "form-data": "^4.0.5", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.433", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.433.tgz", + "integrity": "sha512-5lCAbyZBjtmUt/RAGHRqrL2q0oEFRThDAsZHHDn9XHa89Qw7gMYOeSicBTy+AHfvo0r6vwsZvqNJTQIQy1BLzA==", + "dev": true, + "license": "ISC" + }, + "node_modules/electron-vite": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/electron-vite/-/electron-vite-5.0.0.tgz", + "integrity": "sha512-OHp/vjdlubNlhNkPkL/+3JD34ii5ov7M0GpuXEVdQeqdQ3ulvVR7Dg/rNBLfS5XPIFwgoBLDf9sjjrL+CuDyRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.4", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "cac": "^6.7.14", + "esbuild": "^0.25.11", + "magic-string": "^0.30.19", + "picocolors": "^1.1.1" + }, + "bin": { + "electron-vite": "bin/electron-vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@swc/core": "^1.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + } + } + }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/electron-winstaller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "peer": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-winstaller/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/electron/node_modules/@types/node": { + "version": "24.13.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", + "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/electron/node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.8.0.tgz", + "integrity": "sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.2.0", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.3.0", + "xml-naming": "^0.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/framer-motion": { + "version": "12.40.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.40.0.tgz", + "integrity": "sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "motion-dom": "^12.40.0", + "motion-utils": "^12.39.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-agent/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.17.0.tgz", + "integrity": "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/motion": { + "version": "12.40.0", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.40.0.tgz", + "integrity": "sha512-yjrHUrBFW6kQvjJwRsoiPSAhC5tRwRqNGJWmiJ4CrGnbKp0V88AdzkhBmDoqIsIPfarOe0Uddd37Xq43/gIocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "framer-motion": "^12.40.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "12.40.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.40.0.tgz", + "integrity": "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "motion-utils": "^12.39.0" + } + }, + "node_modules/motion-utils": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", + "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-abi": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.31.0.tgz", + "integrity": "sha512-Erq5w/t3syw3s4sDsUaX4QttIdBPsGKTT1DTRsCkTonGggczhlDKm/wDX3o+HPJpQ41EjXCbcmXf0tgr5YZJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/node-api-version/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", + "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "undici": "^6.25.0", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp/node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-gyp/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp/node_modules/undici": { + "version": "6.28.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.1.tgz", + "integrity": "sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.56", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.56.tgz", + "integrity": "sha512-x0InOIyzgdk+eyaWaRJFH5snEtiImgBgblZ2CyPrLmqqcuMQkEvcDPHbzqbD8eDsSeJbVOjn+crzyzHaM4D+/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkijs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz", + "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@noble/hashes": "1.4.0", + "asn1js": "^3.0.6", + "bytestreamjs": "^2.0.1", + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/pkijs/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/postject/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", + "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/rollup": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz", + "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.61.1", + "@rollup/rollup-android-arm64": "4.61.1", + "@rollup/rollup-darwin-arm64": "4.61.1", + "@rollup/rollup-darwin-x64": "4.61.1", + "@rollup/rollup-freebsd-arm64": "4.61.1", + "@rollup/rollup-freebsd-x64": "4.61.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", + "@rollup/rollup-linux-arm-musleabihf": "4.61.1", + "@rollup/rollup-linux-arm64-gnu": "4.61.1", + "@rollup/rollup-linux-arm64-musl": "4.61.1", + "@rollup/rollup-linux-loong64-gnu": "4.61.1", + "@rollup/rollup-linux-loong64-musl": "4.61.1", + "@rollup/rollup-linux-ppc64-gnu": "4.61.1", + "@rollup/rollup-linux-ppc64-musl": "4.61.1", + "@rollup/rollup-linux-riscv64-gnu": "4.61.1", + "@rollup/rollup-linux-riscv64-musl": "4.61.1", + "@rollup/rollup-linux-s390x-gnu": "4.61.1", + "@rollup/rollup-linux-x64-gnu": "4.61.1", + "@rollup/rollup-linux-x64-musl": "4.61.1", + "@rollup/rollup-openbsd-x64": "4.61.1", + "@rollup/rollup-openharmony-arm64": "4.61.1", + "@rollup/rollup-win32-arm64-msvc": "4.61.1", + "@rollup/rollup-win32-ia32-msvc": "4.61.1", + "@rollup/rollup-win32-x64-gnu": "4.61.1", + "@rollup/rollup-win32-x64-msvc": "4.61.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strnum": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.0.tgz", + "integrity": "sha512-sHrVyWWdq28RbhjuJdZsA1SnGRJV6NiXbk6AXBxDOsgAcA+lmpUZCYjOdLBxkXMwis6RRe7dlZt4VlIWFVzkmg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "anynum": "^1.0.0" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/tiny-async-pool": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", + "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.5.0" + } + }, + "node_modules/tiny-async-pool/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz", + "integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unzipper": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.3.tgz", + "integrity": "sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "~3.7.2", + "duplexer2": "~0.1.4", + "fs-extra": "^11.2.0", + "graceful-fs": "^4.2.2", + "node-int64": "^0.4.0" + } + }, + "node_modules/unzipper/node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", + "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/webcrypto-core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.9.2.tgz", + "integrity": "sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/json-schema": "^1.1.12", + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zustand": { + "version": "5.0.14", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", + "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/VirtualDriverControl/package.json b/VirtualDriverControl/package.json new file mode 100644 index 00000000..14000433 --- /dev/null +++ b/VirtualDriverControl/package.json @@ -0,0 +1,66 @@ +{ + "name": "virtual-driver-control", + "version": "2.0.0", + "description": "Modern control panel for the Virtual Display Driver (MttVDD)", + "main": "./out/main/index.js", + "author": "Virtual Driver Control", + "license": "MIT", + "keywords": [ + "electron", + "virtual-display", + "driver-control", + "iddcx" + ], + "scripts": { + "dev": "electron-vite dev", + "start": "electron-vite preview", + "build": "electron-vite build", + "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", + "typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false", + "typecheck": "npm run typecheck:node && npm run typecheck:web", + "test:generated": "node scripts/render-ps-scripts.mjs && node scripts/render-audio-interop.mjs", + "build:win": "electron-vite build && electron-builder --win", + "build-portable": "electron-vite build && electron-builder --win portable", + "build-portable:x64": "electron-vite build && electron-builder --win portable --x64", + "build-portable:arm64": "electron-vite build && electron-builder --win portable --arm64", + "build-dir": "electron-vite build && electron-builder --dir" + }, + "build": { + "appId": "com.virtualdriver.control", + "productName": "Virtual Driver Control", + "directories": { + "output": "dist" + }, + "icon": "./resources/VirtualDisplayDriver.ico", + "files": [ + "out/**", + "resources/**", + "package.json" + ], + "win": { + "icon": "./resources/VirtualDisplayDriver.ico", + "target": "portable", + "artifactName": "${productName}-${version}-${arch}.${ext}" + }, + "portable": { + "requestExecutionLevel": "admin" + } + }, + "devDependencies": { + "@types/node": "^25.9.3", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.2.0", + "electron": "^42.4.0", + "electron-builder": "^26.15.2", + "electron-vite": "^5.0.0", + "fast-xml-parser": "^5.8.0", + "lucide-react": "^1.17.0", + "motion": "^12.40.0", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "typescript": "^6.0.3", + "vite": "^7.3.5", + "zustand": "^5.0.14" + } +} diff --git a/VirtualDriverControl/resources/VDD_Red.ico b/VirtualDriverControl/resources/VDD_Red.ico new file mode 100644 index 0000000000000000000000000000000000000000..bfd4adcf966e236b5022dbe26c2f26a8db1c4ab3 GIT binary patch literal 270398 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%0Dm(T3uG?9*#POj0LcgX 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%0DQlz`yuyFP`nbehFjz788*TB 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 literal 0 HcmV?d00001 diff --git a/VirtualDriverControl/resources/VirtualDisplayDriver.ico b/VirtualDriverControl/resources/VirtualDisplayDriver.ico new file mode 100644 index 0000000000000000000000000000000000000000..49ce594e4c3e7def2395b438351e5335f209986e GIT binary patch literal 270398 zcmeFa2Ygi3_C5Z7|L-f}Q;I-np?3%b5<-9^)FgxwP(e||h6P1L0TIQ974?Z=!7gIQ zh83|=Eua*oDM*nAB2tvzVgJ|K=bSrthDm0ksNlQ!*-px2=FYii?Xq?`fBW12!+-qu z|Nd6(Z}qDE{cj2Qx}N#L-~RUhnV<99zu|YmzhDJ|6$n-!Sb<;#f)xl>AXtH51%ed_ zRv=h`UAXtH51%ed_Rv=h`UAXtH51%ed_Rv=h`UAXtH5 z1%ed_Rv=h`U|D81or>f zyJx`_2P<%-Rv^Itky^>%b%GT*_EsRk|FL(^f-4SI;7F}Nfd3=4lELc)D{$a z@16x$9IU{RT7dxnM`|U5*9lhO*js@B|Hs}v3$8d=fg`m7)2B}_fnL3PzWVB`0}wev zvSIGrxfdL%YYblJe|rVcr(Hde-(?M|Vpd1;;>C-fKpoIQh#U#Q41vwxe*5jU`}gnX zeXADSyWi#B`7u!wYg``kmz-yU&-Gd$zsnlDa`526w=n-_`NoYK4?*Ne2xbWF*|~G) z{6mKhy^8xr3GUzTy9RGzjc4-QcqYD^U0f1o$s}eS+TxEAUrdf&XdzA2@hW_UzpwyY}po-FtQyP4?pX zzee`q?~au0$7@BEJ@|P@_TcMZFT3%#PJYYl#QV4Iz4#dK-yHrt|K|^V9kE{i`@nVd ze_zzExfk5u6m?$|b?+7MeC}h)zJ2@R)mi`3`2Tj*Qn~H%+vV~*2g;Cpuac`MqakA; z_YIYyln1Vsp$}dyS37wKUq1|a1oG%Gm0^!wBf}ma2DwJAdGcDhW}K00#|@Wj$3vbT zF2kR>&d9UZ$#u^`o`<{uc@Z+Ld=1p?r zTSjhrdxYHd4rG#%5tD9~5${4KL*BbZZg%qiEpp2TkPm(G5x)NTRwGmJIMvEDJWjh! zZk=vr1|BK=UcbH_kGIQhGYgPU@bh@2fWPN!oA+_=XWtv|&+Ag*wep(X_4ARRhvGgX zKJU%=+};{sEk;a+aDBK=5M4K};aelHrVy^}8x*WL)|%_C>(BdO?+Nb@?~~rI7je&c z?{E)?KL^2mb;A3t_x-8iW-nk*40{55;t9J?9=pcdJGzg!pSZ8aLa@)c--bSby?6gm z>_fXBu`h>Eut$fC8X|-5f&2$@_h1=BxoePIamST1@U|=Dg*RT1J$v@VqXYh@@&EDc zDbl!SvK*IooRn&Fl9Xy&T25+LT1s~)Bc-#;NSPc+$Ffqk6Qpy3le^Ck804DTQhRtE zseK*f`npo*#(Glcrg~EMX2>m&TkA``+aR|??r0$OM?&s|+|@uD{HLKbxCb%{a&M9} z7@Z^y$22l>KOP@wBuSJ9A!8d$(nF1<5#?b@vNU?c%A}&J&^*#B%e1BXAuP5n2BfQpzyyp9iG`P=v9{pVWeEgjFy!G!kpPg&K z^{96{1na|fs(VX)sWZZ?U+o*~>UwfrYhitBU0YjfVy(IET>lzFAh;iTUvPhTpY(q9 zuWs%i?j!GKEfAEvx&G`;q&Sd$c6?X;vvInF(nHX<1T=wmYA7ceRp8c`n|6jm=={CSV@L#&U!M|c3>u>OH?u%j{_sLk`-0fNh}efX>|f@v5BwARgCG|BHLe2whXMcCi`QSt6DPrUiCt~e*&9d<3Qlb>uN zD)`>}dR$XUjwMg}p5tBvkB8|C>IJK3R3A_$Sbe0rfY%FM<>>-`9_TabHKPac2t80= z^*}?Z2OU@s`mo*|z&_TgF7!Yh=uEC(?GeB~*0c5vb)^>8m+M^9=>V*M4b=fgC-eUB zK2;lt`vsj{759(&oA z0e$`TlrN@8(_Sg^@0S0TgbuLn*^pe|I}g$c_og%O-4*vL9~keBdv`MARN%Y^?&oQ+ z@23Ofy%HtyOx)|HVEI3g zdw`8m!12A1(ZJ1p!0!XV6Xik3Sja=b?Zdz#q{*XJ9!rrXkEck}Cn%}X^hwA#BPma% zipuzAl0tdfNGcvZc_vL#Dg4s$xHec=7j`-1yZ>ni96+^?EfLU{iMn)_M3KlH*S5Zq_p@2VFk=;2&WBmFr~oBll%l z?9Z~;r)AnfH?)PcfqrNW-H-{%C?h4H2Tn{qQ7##NX<+~3PykcD1pdze{#%^@>_Z3V zKstu-U%rdQe>dQt*gwT$ANS0|{uvhgXYu|5^SIYl&WBAP_Ah{3WU-IE5r+MN*gIF& zl3Et~wXXv1ul8f#?7_M>a8Gh?nmx*W$~~*t#~yBYm)X+}`;CbG(LU_oZ*dQ7Bm?`& z4~65tDe$C1+$+`${)oYpr!3xq%Mi|qZHwh+@fb~>!_Nzn$mhG)qRz1TM0Jen0=z%3 zgZjW;o2LU*4?q`qdZ4MN160@Hb5r*j9gtk04xk=2IskqGbt`o&bS>9ib--|=1L{x* z;QmlYQ&-m+*82pv!ZI-vSx5bAfU19;!51Go>=9ylEU8$nxLzh=p>{l_^KM$A>!+!O{?Y_mnv-`Da_p!IQ$7pZr z!tNYq_bu)X_8Uq=*nNlnhP3+;Hop;ZZ?W%eq}qGnrOBhl)*E{dY{A|eyoWGvu;{bd z4z~{1&!3x+X3h_206#$Wz^%{$em}tJ0KXq_HR1%P1Jn33! zy1@R&Vg6;xoTW=N@o&ANE0g{=%?7+=qR&`>_46N%gTWJ)2LT-`ah(`Sfj_ug`t%{C(Pc3h+Q1 zsY0C4-V-~{)<@cShdtuZV%@{J!}g27xs|jD=@M05!q1%)@ErFV`siL8y1~7#K03V< zrVD&Nfz<^jE}$N8aRD9~>tGGkAGi;?!0G|6SJFK;7GOL;U5a(B?|cF30QCjn10eRI zE~ZYdLtnu90rb~2X0x#XeE{Cy8uSC816(Yiet=&GAU@PMkvf2Jqu&o;TuB{3Uz0jO z{Q$Ecxi5_m5G4+v9%x-w`GDfh1N_Gw|9@%mpLHUzkM+&7*mp6%#lGkF+n7HL`x^5b z+g}xaKlMHJzQI0hKjK2}3C8?tCp@3u_>v*JkG(~^e}j$p8H+0RU91nAueirvWvtIQ z-;aC5yt>a}^VH90e6O}1*r$yo=BqflmFJ)yApha~0mcQ0k&;FM`_u#K3xGc)mqJc}df*lt4=@&>Tn|wlV0<)<)vO;- z%f$lF0iGY=VgZi}Ffk!x0on?30qO@h9pL8zY#iYE0ptS6N97qCRTgo8asl)Kd|W`u zj8YQd-*hv_WB;eZ{-^eV{cjEY1N&eLSCXSBm{cVf-F_T*vLHukUz0 z#>hTfPnJ9fE?meLs z+_kv4BJ~BIE`YC+;&cK17{?Pp&#BH+j==c?z`yec$PK73V00*b0s2x6?*{&<14ddM z0AA3<1Cg8nbU+vYw?3p_sn`^4w~;GbN8;{(YBI33{P0P=xhT!7)G zlnbN|AQwm-VB>&N87E0`o%;OXbv(NdzNgy7h6g4Wq;}u({I$Rr8?MjT{W``kvbmO zc^U%TG94kw^w+FR#3SW(BQ0KUB`R;=F+X44|LO8x6beu%7+~Fi&|iBrVvu6f{eBzsGuPvC{55dT!*cvK z$EW%I!?2HdUvv8*>}zhH*hijPV|{1$$zjuP_51q{_sogA7~kjTEAEMTi+hdjY4e?r z?{Hrj<}Kc%;`|+8-pQm4X;G-~xpOULfIjf+0*^N!UjTnaKL>O{7)Rh@ zg7GHaF*yUo1U4Q}j?mQ%I8FfTO5IBT%IN?vKTwzsV4VhHfm+N9pf;ek%MEI60Q>;; z0bE|t@WNp^fe0=@bAmn}K=T1!96N6mzu)Eh$kVtQAJ~44`BCGew%_CXX!i{VtX#0O{jBW?%kTTRKE=M_ zn^}jVxhOyO5u18BePZA8`(7@eHVxQUPM>z)hkecA5%>Q19@tl3n)m_sJ?_rgd1r(1 z*y1gS!>D4G*tJ5OzndW~{W3Y?d;MJ|o#sCKI{AF_lMbiaDB-0)j+IA zeP8o(+o3!seM6znO`Tfd`H48Q%jZUTn{}p0T!#u>9(bP?6uu|l zUv&Z=d2RGVpqHAzj`)Q73fJ#=Lw>GiFG06iet>a-;Rje(1YKZ#0mBOzUm%&h06781 zP*x9wYKG_sz#m|KkTq7&$-F3`;A-xYl9dEfP>PypzeqPY#gAqf%F3y2Y?GC7jROW(o!M=_`k8gY!B-HW4ZuS z=fVD;5B%o<|B-ed9E`{L5&JgJZ?JFeK7D@Le&+Zs->2B;Jtx;szn?xKd_(2?$n_zI zODzRBeG3>VG(p2NQ5`HaoSUUeLw#lErmcAwiA-*J12ecF0r-|~5k?Za^o zj9c6jbF|AUz@cK@!@I$yv{cv^b6<{P=7%WIKEJ84s5-FavTvHq4I-IqK=6CsKx_E2Q+bYLyQMBKL}q- z>jZo{K=Xsp&(zf}Kgil__yMj~IHUt`?===Mwm|(s^#yD!;QRnr8=!n3@Xxw{-l4hx za!(!?NG<@}w7)LM#R04fVvkUXOoRWR|0gE=Me*M^O^W9N|6E&$*7;z~75moqEA}7aLJ_zZ7=FkJw0pthh3wV0q1#kq63z+9*ECgS`=m6t4dG*AsAJp7{ z&8gUWAyX^F+(5_=psv=MELSV2v4Dx`=-<_0UYGe@5hoB(VG`$HH* z>b`QdLCyy_BRn5KE}YH|F{2&oRfNe!s^1(EEPA4|_ptdi~f(J-^oRBc3GJr*4;Lx&XSy`3E*uP#(b_ z8#sR;nlC_ZlN=#)1<(OrzR>9aasn<#NMC?lir)_a&#HC9%njncXdcGK0?Z9+-@wf_ zH%J}8o*~T((%&;WfP2B|fRGPha{}}Mr~}{wFo&#Mz}eVq+;3I~__NP~7;x0Qm0+{4>TnwPFbSe%sG`Lj1d2UlhNeJP%{NP@a!| zzp3dDujdE$wXV-#U;Tb!-(S;*c;EB+8JE&7v6hc}RO@+Q^EIDO?9;B<{q5uSG|s2L zPwW#LwE6Iv)pxeBz54j@?KQ6V+j?RTSXA5-lMbT}>%h2Tdj{}5-3q^sB{S`F+~4rg z>4@o8FSz&O``c?^Y@vF9x&YV7>veiS{Q;jZz*+<^CgA6Hz5savs|VB8#zu^4^tz=O?X)CRZAbh_5o&LQ~Z}{|q^D9*Ms2aEMW5ktdU^75Ok}j1E_Od-7sSTrvu;zs19I`262GL z1t6YduK@3J-Rr;yFfZt8gKTYpazd^)h;;$X3Ai`_y~3UkKpoJ>>Hy{h{9GXRsd9nN z2k2sT0P0suXQKz8O)2R=BJls?vH#PE|BHbCPQX90@8|rm?%|xDiPJRC55LXBKKXve z{Ivb30afnT*7mwyKE*!whT47P_&n^}{66=TtL69C^*l_j54ka z{3op$fLc{}_Beea5S~Ki8LFQpBC+K1Urvpq*0QZ>rT#pZg9{?Xv`vo~Sz}5!2 z`arKX2>VI%0nP_dF3{+JM3)mh-Pb34GI#)&3+U>6fbvo%z`rT}$N%HQ|1{vgxs>c& z%G6wf_jCC^XZyqae&}fB`=PVL`g$zi@Ay9Q{W`;g^?kJc#6I~x`ic(wupg$j$L062 zw`^|T8Y&;9|zm2Hr9`O&%OhPwAEpF z2VNcCX93@o*?wt{?ko1#sU%b0j6Joc|p_&C>H?V zPWeDv8^C@casjqq(A5X34}kp=iUSJq0T2ghF2Lsl&_;#T1c3{5J;G&kPy^hS_W!b= z{yz={Fm*oge{s5$>;n9=&IdYH@!va!?RP!BjQOqYr*5z9a(%ex>i=m!pT$0HKe5k# z9%A3+_z_onc|O<~+8gDeZM@G~-W24k)aPd&oAEw;epkQi<@A})=RS8f-{tcxr>`+S zZKd+~zz%(CVxPE&c=3ERI}dEC9e0>@*!}|erhM66+B(7aZ7FlC@aO#A{oX$6-|=t# zx&yE8`4fKMfI7(OB=Q907;(*-U!ecu^98gX0bHhu2@nf#9U|ia@B&;n>HvSeknsZ$ z2e?>(e2dlxM9~5C*_abxuK@06ed++t5wcuBJ#qoc2f`Nk;s7%{$mRq!9~j~Sy_$gY zEEkA4An`0yccy*9*q@4j@KmY;khAvp2$#jYuu|=S|C@>bp#DFW3LyWV)(`ma3j90l zJHH?CR~63lInS5tr{7Q8e<^Bx;P-27KW#s1y_EC!<@$7X7xw`DL1LdVzr#NE4EK&d z=J(I)@Ot%a@1EKv$Mak49~SHTu}}ZkarE&DZ~-|NB0v|dU`)d2Nu8($~b!l45Z|c{zj@SA8%wxfBsol5yzShDyyYH~?;(TC1>vR!?`e%NX9l1qKz#sm0Uj3!9bmaYHy%ss`CM& z|+mVZ4cwqNS~kB_xt?n z?_-avzmFPS&F9hIH*qrKWiQT$?NmDo9I3sB%@4Qrz`w&GF$vq=j^c1jY<~r;JNY_W z+EW(T$HKmL&vk$6^n`uwNL}Fk4#yWLXNc>Kst247@bLmBSLosaa018=Xbmyv71(;A zs5(IT09zkGAAtP=wpLhm0QENRAAPt`Z4mJ9bbyTmv`0X7fb9{8<^v2s48VMV%LQEM zd;oBO=fM8c2Vg$X@&JeV05%s;HV^in_`hX9Q2!ez{?ji_H~5cc`=Nuqc|OkXx4nHD z_xt>Qod@I1?gRIuoF8lZ-TWTL{7E6d-}dp+@7KCsaQzj8}m7!&X_e9E{5 z0)8OO7jV1)^Mo1?0Q>NXbe)tFKpa3F;Ex56N8vhaeE@VWb+4Bb@aX`}2{=BGGX-r< zkbN7R9l|(|T!8io!3HP~fLYGugmj-cAAtD)KNon3;R3ZDfOWyJJ*Gz(dVoBD%LTA6 zpeORx^Z{5CXnlZ$PUr#13Gg3p{-5?gy+82Z9roYF{q(o|`F`kP*Vpgr{M7a{?zeS* zdQXw-_v(8!*Kgx~_=B3~cYHtgiuUu-e%M@}sqHg(PNye z*r&g5vG44@#`?s*kKglRe8$;6o3D5y?qKV~>^w2=aO=nUl3eLP`6h~di=R;hgPR9#S2Yduk9RNK*KY+Z5)d5-? zfqFsu03IJ;>xI<^!1`;>)vp75J%fe|FmV8Ny4D16UW=Cx@bLh|KjQ$$1v($#N}mtF z7=S*2;{mic$kd*3R*>cbh=0_ldA)&_2VnnNS>V4k@IL_l|F#{$`9HBVfa3o$;Qthh zfB0C6f9PD(-)lKP+!K%UhpyJ1P8;`YpNGx&`E&hVtq=S9STi0`;}_!lfqj?jqn)wy zdcyL2tnY!{=N@(Syk0#Y<9+4$;qSxFaj$zheOJdb(_lkmeaG(+_Yjw-i;8>e*L(Ke z;ns)eY~b5SN6B6qkGSVY=mx&F(<}5XsBb7v4>)d-alu@t1MnFleF2v%^y>iD3Qz|? zHyRy)ULwm0rjrkVAK-cjb*6x=6V%)w`9N|38+K>=48N#D79p*#BJM z|JH#2i-mpup7>99_z&yv_1F3V`^0~Q?N{F)_fR>1)_S7WgE?Q;fopv~ZNJO+5&OvZ zh4KB!C3$>5_LS%MyF4FlKYQ3M_O-7KcFM*4^kLQKC&&Gs@p0Ae!{67OKCy58eJ`iy z_xGKzPyd?s-ud^6`D_!z6OY8D!#lCQ)F;b;cglAm$ywe}q6wdqs4;R^976^a5Y5A3)pzTaRM$!7}5cV1Jn=52#W>Wd?Aek$OX7K zz~chI1yToSJ^(pE<^!k$-0UFj6GUAwd;qNray3C32N3_!e1ItR0G10hv%(w?Kpqe= z0Jtj82k2=y0M-Qad^*MdEy(`|_CKZyQ2Y;o{XZ4uJ*B@)lCMM;TM=gItS? z`?>DS#YVIJTI+-OP5XP`%W1xkb-u9u#_uk@jkgN^yL?beeh#mtdF?aarq9o)v+I6* zugsHNNX{xeQusX|zqjIk?t9GP^C|9ioGyUxpuU9j3;aHT^95{7pgO?w1*{Ih=L^LH zRtLBmV(_0@GYB1N_yGD%J{{oC3u3)B4q$zdmk)4UfIki}bpd`KKzRVx1Tf|!4}jcn zXnwHe0i*f=&;yto<;@F&&#Aogg~k`4 zYWp4E@6Yoy=6627wfmgM#kn{h*9Uu+iCG)!^E0m>&F2r}_`vVOc6zvX{=U!GSKI@G z^w&MRzOoZ=ZKUJ(ounhb#$)d4&eDPJw4#^1CAG*iug!*LHGdBn>vS#IaBfh z(5+e<;QRorzs?VEaR7Y)7YD#+V;sQ#ES)3BS;GFBfV-gsI4{Uw4}kpVkb&Qqi?=#jXx6gPQ z*!MU$>+7qZPu$V=s?EitjmNX`9F=eJdmq=AgRgU_FH~UH`AFSCSz++r5xDONeUL{z z0o}l_57Pzo9m4zq^BU9xUQFP{1JDE1UHB}-J@M|<1(5G_cn1#>)+gX{fvg39E=_m2 zK58!+NTMvMJLY_EkZxD0J z{+dAe0A@Z2V*tYg>U;pis+tF648U9FAMm;xaxnWEtoDXt^oc|FN^+u)LRh$ zuE&qIAM3B!hfel5fA8#`h`67=Ke!$}GlKa(&hBQ7KW#tl0&Tx?ez5zn{idgv*f%ys z`F=n4x$jW-!{_mDwLh5WemD-{0oWa!W08#6Ks@t<@hSIj=2X=5IqZ|)hfnO)?s+*q zU!3p5J#kB{Q9oo&Z=)Q8>KghU6gz`cbr1PRzc428gn2fTk6y~AyhucN)D-pGY7 zp}Jr#b%7Q75cDfNzX1Bd=>o}m8HC>-D z=eBsW0>}l@2jILg#sRDef{pO%f)NLpIU&piU{=_bhymD7MjwE=0G$zHY!Ue=&I?u_ zfcPg5=y(A3uAwiGbwFi0mz4px1^K_2yA3oZtR?kEHIT+nHdP*ju?lrk4&IwFL>~2k(*^Vo=qorqz_SBg+CA??3vvD>?AI!s|oCWEPvoa}XU`95iOfLv@eVNnB!snxu z!TMRj@5-HBK`LEbS*i`HA$4x3t2&%ImN-{lmi+;)M?iH8^ogAtL_UYlWldKo9~j~R z!r}mOf#?r*v%>hz6B+>ku>g{EU9OuO4jvlr2Vtm-~zJ{1EZ!- zv5#2a*?s!^mgh5mJp6guRN7YBSnzSQ{mo$SY7D9+wR_Z-gxhu8?{PaYcK?0jzPZe`iCfa%NR&(5;~)cY*Eg)L0rr8s$McQg9FS5np4i zRNUI(u@QyO!Qa&DjO)y)hO@L1q(p}jQX;>kB%E7bs$X4G--j5dKcT)F^tRS%F%Do& z5O@Ik0FCHNs1Jbm^VI}7AHdZF>zp9+fY?VmF9LQ&>jAOu9{%A2m@@;w1%e0k@XuNR z*AGA*05JgTfyxK?C-n7S-1dJ^E7%2#|Gvi8Qr?fgKV$z${JS1Mhkxe$fPKyRBkrf| z*E&%8{j9VxKn%cG5xQM#g8jJw#sHib z>UaRg0N977FUaJiy$MZS6PyCb9C;l5h)?ty4!WijwM=xnOJV|PgsVixdTPU|r+fN;!F@cQ-T<(E+M&=6mImp>3UQtDwoZeIx zEnOVz?ThsLJ$v`a4?q4OUoMy<6W)DE2Hi7AnqQbM<$70;%7d!n{w67(;ME1PjzePr z;{zxcNc{7BamxdGeL*9r1DFels0VWRN35wnfDivwFLLEZnnn%wEt(q{)1o9*?yYyN8aDn`%xFeC(~O0p*HqMOs=v2jm-JO{zIpmI)9t* zx99coJh{f~@v$>~wAP2VA3h^%Nu2Fx&zbuEWWU(fDzL{}B(|9B&_a+}G!S;+{Qw zWZCNPpi%GI!xz|4Se7uQ&d@Q9k?XGr4Wt?b711mdc0Jx~Y!xuR52NaR7Wf%>^G;_j$fB25t6Tlb%e3Cmii2F_Z0>M3TA9{Vk+@p>IbR2-=01*R} zKNa>rz<-ha|CWP^{{+kXqvnsj7nfS!pWL6~-<;*boWD8CBb@isxlhRRs_)Ob5B7tS zs|W8-{Chn<4*T@|$@{_1tM6~;dFWYI@BtdbXP`d%N8NwQ*_WOxTefZ~(tiH-mv5!V z%+n-eP8RGbeSVX}(>ncSCa31&X|=1sl*`|fBTRn0DLBTmGUCDC7yteb_5F|E%TrUv zOP7c8C24$P$@sjra)8tU>I-;z0;305qrkI?stl?o$*2AP_;=U3edl)h^s7&0;Qa%o z_9bNVAY^~Ryu!SL~zOVFIc7}NxU1JD^E^Z~qj0OkQ8HW%pC1Y>{cED7>}@B!$1 zD*i9QBlacF3NiJ-@BzpHKnJ*50M4ze&=WmC`3bQ9m!tkSnE&POSwWA}7R->ALo%dH zZ{VLff5-c~oIlU;qc6rjU!Cn|>V0^>{Jg)~e&C;dA4#bD@aB5a_xJIBUffR($?|@h zW5PUIa(=8mr9R-SAo94S8PHI@p}Ojroax7w^OF zE7wlj&$t`XVS$PNJ^aU4M_%By6y+ED_V1H9E9S}-uMGmK4sRQuH90B72 z)DMueWDQi+KN$b+^KAHegFN-#)6!}fe2}44BNDJ{9&jeWAgBS>Ru1 zFIxP&GkoUX5=%=XfHVLRKOXSjZ?;RS!@ z*?r(k<85Gz7;C>USCWyZ`!n$GKFr$9Yvt~#cT0;2sB8K%(_mk9faL^mt*jGn^zsvjMJ}~yz zdSAuAwf&CwC;n;sZJ)2P{l@oigql$L{@UNmoFC(U`2MU*q3`eJdgv@a_MN=cLUTuz z&#NNG)h{OJ4>(_T?A%ei>5S>_3%23@M9Rk!lwq?)UNjticM!{yyGM zeSg?~AO5x0ANd{jelzE5YV-^b?BUQr16Y=0`98y))Ano4K7D@LeG0JO>Bp{;y(CwfJZ12IbDa1; zC75sZJb>x|#-7-R^Z}}IHW2c`)sP2Ze@e6%Ai%$8 z{^Q1fE8-vZV#I%S`v0uCr0uu(SKl8xS^NCK`|FG^`u>jhr|-``Kh695e1Dhs!#>bH zA3y$C?`O~M@#gr#{1@UdZD9<{0=%Bp6?gME*D@WQc?I5{c z%x^e!jic4)r#&^;C%#x$r}*!YAo1dV{r;b1;c{1=@1KyzR^oBR<1%i=czJczYclhPnX>AaRkCGw(Ky!w2M@~YU%V+T@63?e zch@!Z0W=R_VnK(0?vuz`ATI{M{_^qwi~)T3H~m2917Izn!@u(ZzyVaI{XY%yKka|O z|HY60p%(vI?{D&c2LC1pL;Qz)|M1vfd4KqFuGfdWKWjcz=eyY7;h%NoF6U3)-#^Ej zzCUVy_;-otBuX*({{05^6Xu|Q%fD6ISIN0^&X>$Ztzj=wYYWUUf2Or|wEg7NVE4(d zb^58RblL#?e*^rFhy5=C{uk|8EPXd!B<;86NayYOkgi6y`I5h}yYyOnrVL&= zL?*16Agi~nR=+vk&o=L;dD3UrIg;^BYrW3A^*;PxRb6Wgw>z0a*{uz7XP{9IqDxuoj5+-{N0m0K}Yz12FbKD*kN_fHOfVLkB4SPX_+) z4EVozZW{gnR=_{&#hG8Bf92Kuu@8gXUs&vK@UOluVTC zTkI20#D3?ET~U*tD@~tHk+MDGxBrXxE|v?o_LB}fJ4%;b-NK~vF6e`u`I5K2i*(q4 zT4V4F7k`1e!jPey?*!0gNPqqSoxx4&qE$_Z4RzAA9}2_u(zmM5%AC2h;eJ4 zlIF8oNamN|6~4esfZ6B|{0uS^Jxen%7i`A4RIPeyXk9^6ss&{Kw_)Qer-S zw|=>tJ?~HdZhWiWe@rUjA z$NsM72Q{S3`y7{2OftG;%D3Nr8*{J4dIGtrsq3do?i}>=EzdT&KGe|o`9AvmDvJMn zVJ~md@xMg+ZM#%*cXg8d-6t0)U3Yhvj=yx4j0IVe^LCy*H|KfTwP#nXkDZ2SXumg zu|K(gU+i!2@8kZW;U7LcYri7#@Adjc)coj7PuBZme^flHk{n;}c)5Joz?gNnd-v~^ zox69+frEu}Ym5Od{`wNh`nIk1+$#1h-=~}(@ekjhw!ibHe9fyjeI`}Po>m0=zYqA| z4*c&1{`Z_L-FBZM-S&ipKU2NH-{I$-e#wUoZYizDx0TnvoG1ql6#g09jePxwH>LBE zeChCG4r&#;=o}B$|Fg#^9{=CN{~X|d9&pcDA?QoinPH!`)4s_p;9qA3PHAKMAYV+8 z8aLOHzW4W))$3QgODXhm-7o8;|AhWh<1Y9Bz z2VgiV)~a}QK2zcE{Ho)3|Iggt_#aTSHxIp$UvVCinHxwQV0-|^060GYvx9V2 zAm@Z3$H?m_?5DM|9;QrMf>6qlGL}oeVa76 zsG)o~`@?8Y3H#Z9cdU_ti?5KZWrzX%__x@n@2~j(rJMHI7a9Nb|98Uv7lePO4}AFV zzVB4^MH;`@L|Wg}M&>P@7k14Bee>CtS<-t|A8E4+{yJt9{jT_j{b!DmeTEGmY$S<& z6J_Y=p|WdtY|etU%r{LQA$9JlCrRiJC^G&T126}0jNl*li#fOd_j^jXg)jWOg7%O9 z=k)(GuFi!0$BYNmS81&!bN}RAY~63D=Lh~j=C*nEAKb00`%(MP+#mb>X#e5+vj&`Y zoi+a&``em7uh*Y7Kd}Ft0l@rk@pi?fO4BOx#-um=ml*ZyO?x-Vg>PIWC!Jeb9(d(J z*}J#!ociwldt}_|r=<1bc9Of!4MxQ3Yj6#v73f6jQJ z@6TFmhktT^$iXQ7qjCSN`?1P!$l9OQ#`+WC-<-UlhjdtzYxsWV`z`0k zxSv?;vIYJ>`hioPFS`GK$*#*JZ-18pvG4SP7aIWoU3Yd@9tXLBBbMOhT{`{JB{;sREnA!?63K9RG5r6*gY2csr5PIebW*xM%XNhrMB4-ep zJRmp#a0|)-!2jo&L7WL(=k|Jf-tO>G!(;kfyw3Go*Gu1*FOZt|)VBDKk^?{v$;<>p zFVOXNCLs13IY8!~Lijg10IdUN4PaO=u#NxEu<`$&ga5wx$b~ziT$JDNuW-bN|2Cet zariF>`(MLf_j_d&{8KjaPn!4ROkSP!1N^h+N9%qf^8Sebn<6JbpRah^ zl2SFLs=Pnt{g~X+jsrX8rsX$Fv#H1#K9MXn`qh+YCqEaHGuV4zue|WX3(|H;d&ynj z$>#eI_Y?aL|J#WFPQMNR#Ck#E*9&~kUT^@Y<7x0Hbm+y6WYY9WF+V@=>-23ir04Qp zlJ;@B&L}N1{=WkL7jP~je1LfoF@T;UYR?cweu3wQa<+PlH^Bki+ei{n7ukDo@3`io zFh@J=-D{-wy>+!W(B%NRUw9s@s{?j3!H9p20f>K`DN7D868~NuAaa1p0dgM5IX?XN z0RQ*r;QxHwn%(i>?$|I`KW16YI5_?aeB>w?_51FukIgx7p%q8_@Dm2zvmyb8>z3u@Q;{| z9KLdY%Kh6}@{jrLKl1;s-yisA4Inw71k`I3uUA}d8hKM(zWKJ0(3 zb$cCYH>ABRUJ<)K?Z;i~<@)b#l+2ZFQR`zlf7*WFzw6fDi2ptC{||PNlMnTXDu?+5 z))(lE`lF^3Ql(no>T<_pBW3@-*dgS~J>N^e@B2&Y$7#R9|9?g@i2sE=1Bn~}#Kr&~ z2goyo!7Z{5$Z>$o0g$WV9P#v?>GI98Z{pcv^2n@5qzP&O>yK#|;s9y?Z4Qw8D69{_ z)d1N#Kr;^%c>u=$h)<*2f6fA`Di!Jf2lihTK|D7{@qZ2Qe|`b@*V>=o0spA?BQHQ+ z;l#}1QZ2QbO#5_N%u8nP+^FwIOUo}z{Tk1^PkN-W)VjEiyfx$Pn7%W6)Lvfqvb6ga zwZI!YhkXA(1OJv2ApSZ3mwD{rqppif2d>-qqg=lF3WI-ea_axblm7?*_w>3r@y{9n zhkxb(F>5#(GhBF{KoiVEpYv60&V@I0{G6wy+1NBw2jp@90sdp|^=MBBxA2937wzvO ze^xO5Vf)$lNBlz%JNxhD{%HSE_s?E#>Tz=atnHxvr%z8i!Q5YyF#8{h|MhGS#0piw z;T)fIymUUTvuxNH+kaoRbCq1S)R)tw^5s?KhR1GcQ~;VgHGL%>S5!IlZy|vHn;2Kg>j-{Ws?b8xGLq z08j_?8Eii}0OFs$0L%d>{xh|fljn*gbxx8`XMY;&?ezE0&3j&&Ka?Jx1N7q``$%U2 z!2j18K-K`E52%iv1ypk|`_Le$NeS71c^BFj=m9<*x|A*n9 zwZE+Kh=TtpvpzZdlYSq2wM*13DI@P0DV&8H>)(`3Q>5FxlXWJRp6P-770*ms+az;U8^ilE?`QF!za9NK=nqZ-{>uXYH;ukYwr$%M^EHY8 zOZN;K9Be{KAK zbB1u?_;+W8wwwg~!~PTh4fB%ptbv#dgD1Q&{{`SbvIfAM3q<~icEsxg1iz$b0ucXx z|6gl>{Qf^@f%^HsqXPfr|6_0BcowaI=R{(>u)+5mzmA(ad3k?we_erp&Icg= zng1K{V4U{<0Pw#z3jT@xc<>J&Ch?rgGUU!7@!|gf@c-YdEdN(Sn#5cGYxDmZU(x>C zb5VGPIC21OzJ$z*u>ZcZLp}V%|8Ll-p?os?ldyFy=$jY50{$Nc{@DlM{D1h1tN}Qz z1~@AIkppn}*BW5X0Xr(~|3z4X_!_wiTC5m9FAVRIzl-rw`s=9p*V^AG{y*_g{@>dF z0`af@KXicl|EBg&`+k6bVxM(nj1_pMU$KV8r001(W#gueF)#P0JwM5?rPoT@taLp) zPwoFg%=SU;3h`QXKsCAd#nExqrhdI;ft0;CNz^ueG+BJUGe$<8;SolKKvIU|3Cgm+JE35bJ2M=0&{?hf7Ah*KA>>_ z-*5j}&rSZXK}X>Ki=yBk{(ltwdo_TFC$$H_!#`rqg6zMY1$q?P|9os>F3OjX*>MPs z-nWq0MW1*Q|3zZFuzCQmYyKIMKD(tk&lmB(p6i2o{Vm=_9nA0=a@MVV~Ei&kNE##i+?>6#MS`&dw|#n z?Baje8IAv=;h**&bt+~K=uzkYpNNlK15z_isgCFMhHQq!^{+6zNB%CJ7mOPJ7i9mz z%M{H2`QrbA{D0*8nDgU#-Q;slXmKKZ|GF}N!TfkGRbE^>LE6r0FBxATcICOA#6SH1 z_BhXzTvff%^`+UsG@1KNe0?-;A&=N;Ip%0>?&RhEBK`lk`~Up*-*5Nb*HLnU-GP7R z|2d1S(c{UcuI?`I|1o+QxL7~z|3NNadj)Es4FC75@Q-s5bp15{XKH|=;otFpJY$@5 zT~F#(8fS<$mbvp|&l};jJ~el|q&@`x|31I}AIkrk9)LLU&-|a-f6M_r%Hsb>{KfM< zmZuFQHt~%o@n1Zz>7HZ$qVWG0h=1n(Bjf)gfq(b`tQD$swwdvF=7nd$2MLaU)7V)LX+Ie2f4unL$NwAr zNAdqX{@?Wg0{b!Sf2H0y1N~3R|2y0j-{2lA)mKLPoFo&ACJ^`H5{{2${m`eUri zEZF{o`w!#)L-BvpC&2+&|DSvydBoCrWu#P{Qu4&JPsFU-C5{(uTO{Xxb-tvb=8y4y zB>uUNo4<|z6!gMfGU^gNW0QYZ*uNe7cE~-eM`;Yuc`NFFqVRul(ns*AD;e z|7nLDMvWV5N@7l;Joa3C^?&pB&X;q4I8V~1qyE>`|HYaAXaA4k|A2d}8{ePnSy29u z_;2+Q`hQTfRR`xio{)8-wCmYke*7tRoW*&=w@<%A>feVMaX1?!g#Qr#ulSEQ{*QtG zKePU)D7bge#r`}h@E^7QC(Qmw%>6tp|Bo7g!t(#Xe+tg~Vs8+2_`lOmkfh8cS+rzP zVG#9xH);L5l06IO-SJFs5C6u0rTwR{R;A8e^(1RhYxxdmYR2>LouA&3F3ZvLyE#v} zf3N=6^8ZD_|F6mav+sy~HWm9Gx! zIRl7%Z>2L6<+wV<wr!L#JOfFr~U)nGRK<@81n@95(HoJ}vvBkuH{kN;tX z)c>;opR@l8tN+*fKU4qD885E?$ISi-)&KK*zE0`f(o(W^NqO$&=i=&hS+Qe<^#6K* zG@F^O`DxCpY=?Ni-F()s!+s$L*kMVI`m$*g)1}(r8Z!LR>xAdj#`?GCz#bX1W{kA` zzJqkJF~G0x|3m#>-1Yy6{W<@OdAA1tNs{8Ni%ZW7d&t&pv1je`oS{+M@0IpHWJ|^j z^gy%z@64j&-&g+=&Hl6gADpVI|4qY;X4dGH%q}J6lgi7>ufH6ZpECXX>5}_QN45Xn z9N1kf1Kw@+nk@w_(u-A z!(t;DQ?jJyZMCKAb@{S({n}XY*WXXx@SdEq;#9-{9Ygm2*Yy7r|Lp%SsQ1_ZbIktW z*&{99Y$b^oRhE;Qo+P6lx+g9!#Bq&|leG3}vV2v1=K(x1?@38{1ht+Ip!dhxf5wTN7oq2X zu>Q~A{}a~#iymO`Pm%qVr3_qorQ)gUk?8-o zeSO{x5ZmXY-|N?y`-Pb!oVCLmxD#5RD2>}TmiY_6ic4R8wC7_veao5B9{mfn(bazA z{146mZA1KX{s*o}YyUX|2(Qg^2id1x4?Uk{^2$oFs>NjJ4MS!3?$~Ya+xKplt3DVi zbsne-{vURMxqsLP?vu#=|FHQVjQ``A|KXqia}Mf%`~126e?`H)d+yPLfB*a+VE;(t zAG*Kx(ArX}T`4J5yOg~0#w&5nk6O2Tom{k>^}N^!1>X4;arsRy(&tvTE*nr+lI$gx40Lj zVh`?oNOFG0`Nf!H=*|BuLj6zV{GV|AW2Pwke}MnYkAZ#o{j}AIn63NI+W(XW%^Jw; zIV^39{crXUUr3i1^QHFvb#xw-;@_PEM*HuZ{}G0N=l>Tl|A%ubc~-T~|Kb@y=K}iy z{+&wk9%J7j5zYVW{68Q5!O!db&j|Z}*x5hP=Kpd25ADCJ0fOD<{D12EYUuSV*0`87 z&u%VDmc}>dYR=Ddq{rtyC2bnGzo_^(96$%g0N?;REXozms;hBhE$Mah8M5)`*mcER zr;j&&EIn78CLK2BV*VfM|DQ+y&*?>+|Hb)#@c&Ofh?;+&Xgq*ApM2!{I<4!X{tM?J zR=J?6{Hxx-B(qbdEc!OSSm>Jr-%9_jmr480I0Fo?rTC})FGBtQ)0qFAF%R?qF#m@f zYCCTRIP5>p0LY}>#~RY^v$vBocK+S;-*Q6Y33BJXcgA%l<9?hY{NR@lO5;ah?;mVr ze1C95tRJHN_vZiM?1->)fLUYi;UDuq!~B2N{&4;`YX2Snc?JObrYlFz|NIl?|9HSF z!WYU8UGKDod1jdAI$$Gr?2z>zya|7`teWfv)+&XV}%|S<)796 zC4;XYBEM|;C8l~}?)cR;uS&*L%%7W{DXqat@ti95s3-@>8bES@8UvX4Kl__p-NSXp z)RVL;n#-Iev3p{9AwTTGxf&~mNY5?$ z^>|7Dw3W2}lIuvHA2mN%TjlvDX9&+@Vt*EEJ$SBWu|~(szY_i>7hHA$a{fQYyl;HM z;*CqB_r$ZLHhh2Y9B|^F+@J0J@oN8h1|Z@|&Xv^JU&KFae|7#Zdw;MWS(6I86h8kG z*bneuptgzUMydVx&Hwglfaw2Y-97xf8X)LruLh`)`QM)XM-LEZzLHBU*}9aJsasa( zd$R{G*1t`A(5JijHc9y~P5aR4lhQtsqvClLtXU@qpg92Af93&!|911TrSY?fHwM;~ zM_zvvwSaqLy_Loy53hMt+Al*M0kc7hI{$m&mW6WGytC1p(8`>t!I+wS8hjScDB`SR z=0=!5FVUfd{Jq-WC8bS@y!gtCan1YS2l!_HH*zU$|0c{W*xXIe|6%Pwk>>w`PkDU7 zUt^0VOhIKz!IxHwz$r0ml2_o{!F6Dpk`7Y(>bzFQGrO~oF@ zQ(ul%&M@gA_=Dhrl>77J-(UOhYJZ?_6#vK(MZ-VmQpb(|W90ll+}1zc{_FXF?))Fr zR)?Mc1KdW=013+hum{+l|3x3)JNp;$zRm!F?Qe+R^PHxC*8i8Z?$TPmT^3)Cd(oCf za@OZ(OX^3>VdrpG?M!e|pF#(Kqax3zXIC;0KplV{HkSuvJwWq!T1XAl#a;ZLezJ8( z?EZb?Yx-u))codj?Tt@)7XCla|BLhdpSj=9mFzJ&QsJ6PQu8M0PQ)M8ud69lhg6q} z7bVI`rwlh1{bXuf90;#%$Dtka(9Va^tB|L*pJ$cj!KcUs zKVQ`IzeZ0QEv3&dBNh8qlEjNEOT`NlrNX%tBmr~%N^~qG$E6jM|EvCgQmk?@NpG7j zqwXIiKdz6@?(hBc|hi`6#vKpFb@bGfahY=x~q<~y{eroUA>eq7xUkSJsV}n zvZ0dxMJs8-^S_I5{^#6p=1PZ)v!#673R1mOb*Y|*`n23?Qnh_GsnwyDG|g)&U3zzs zt4CZdufOxU{Iv0>nAhWf!bf{QlGA@VQ#x$wh`EK`%=y2+694x-d%q;KNRUcdiBhF? z6-ms5v`Uo9Eh|gy%sSGtQ%mX7uaArwJ4U{o_oeJV5PKe>yVf6f{3t^|zFO)%j9#Bd zF#jDjAG!}5_wVq3821P4v-j7-znT3@|5Lf>LeKv*xxZua{6DPQ(Sd*e`M<6Pz~cX~ z^M5%5z<>TH@y|J6#Q&%e2f#Cc_%r$w#WCk2p>BdqdViAJDuq67+qX?de{-LtOm2qx zlbMo*8Z_ernEaIH0T8>gXEi(zNc^J~5VI^AJ^_w-V3NG_;mdK^S@I2!uYN*(_M|5p zN!gy@|Hj65{@>w9OSOoTq-*KbaK zQ$Cpbfnq+M*}jq2$$pB5=RGXV#x<9Qk2X@?S7U$d1KI&S`@?d7%o&qAV(#CI{co_` zA7g*me%c*B{%M!0!Txjh2jdg9|K$G8Ii~TSi*=0M0B0Awz+gYhFCy>MYk+JY5cIQm1{iAqm;-S5XC0s)|G4M%QP<6L8UF#UEBDk~S+;yx zw97c`XDha?l#4&V#Nt0od(b$Gj_1=e52!H!@lPKBJRr}pG4nuJ2Y{ItU*|}(iOr?P z&{}f!Lsx^dj-3bOMa;oL3AFMvGTCU%IgJh#NDQ{C3%q+L~ zhYrYH4g6y^ceSgk8~%S$eEqmb=+eSohB?#;-%OA;FSL{Tk2ciVuACK0?l&y@yxTYp~S1ua1;HwM^Xh zKm1jW%y+9$=XcBYTXjA`$IYEX*vGuUyq{2~fH`Fz{!fpu{`bgSS@^5$+P_O)S^BEv zywp(|Jf39gJ;5a=Blh?3&$=Jw{zG+tihn=%XYtQ{ruc{5VGUXh;GaIE!+%w4|EqZT zH?cqQ&-${mCnv}Nka-n zm$&MlX#cnF-6~Hmcv9NGlr0UO0QOPm&AIM8_f2E}2a*4|pT57z`|H`Cm`v6=W5MsI{#Xoca{5<9f6MI*dzt{S^ zbnTrlt5&ayi9bGJ-OhC~X!c-f`c|s6oSdooGoC?3AAtET&j(;F0DS<*1JVa@^}xVC z``VkLkGJ;Vx^mn2+vBRO=ljoHKTkS5kS)h|`8W9gTjI4N{L|n3Z##JY$$Q_EOSbe^ zuD|0Z+I__PjQJ^;6{z@!4q%@W*M;$alQWyj+&|F&=NY6+e_kdxe}1d9d?7=U#x=qj zZ|L&`{@E+Q`T?H*NDfG2f3^L#?w_$gV#ct#A3y$?_xJFRTnc@E;NLs{HYAoqtXX( zJOJm^J0F1c>pBx`zO4sfEx>{toef^|7M!hnS0DL#>(B8liLh?{(h2>gWUo?k%eY(P z>LZNng$sGo?nAp}(Z0oU?~XCx`OlCX@cbs;x44JhC-yu22>h>u{a@D^_|HRaRBO#Q zrd^yS^S+I5?pYz15%X8X{f1o|<(2PVk#pWZ2Xzw7q|wvKl8V_5Jj;4vrc55$HLP zkjz(GOQYwKr3vPia;78aJ2!o@nKV(~AGZHtWBa|HA20T|Grv9GA9a7U{eJwD`(xgZ z+#ljkf9xNIe`5a}!~HA%&w%|8@b9UKcHv)o5HSmY zF#vS{?x$A=1m2H2fZSlkD-xyu6PL-FO=~p=$MYvbasXc+kOLIh6=Hwm?u{~H&driM zv8klL-BRmiTfUbG9{>XUa~7571E6Nr?*q78fWIb~If3*kEhXc%EO~m-)3OhB=?C#% ze7}N(@%NG~OXQRfddR(Bj~3>&__KnZAM5ArVcdIYuk1RsOYy$p;09TU`2Mxs6J^Bq zTjZ=?&e58lY~Wt~edPE&pWlyt)cbN)Fl&jK`|Ak)pFBs}2Q4K3t?sg9(>Gu*4#j<) z`>_U2_!)SeTpwb6$NnAi^WL9j<@Qzb(b|vXo&}?%&&TIV*6Xb$c>>OMdZDSLVwN-K zxu;?-5a+uX-=DF66#QG;uRXv1*dKM_n)heyZ|i<)`SFh&6!)Q?O^JQU{b|npg%4nT zf9Cz^`zI3rs4*|wEwKMo0`cH~+5-6hgInQDH;aG70K~s?09bdO1?cAhZ5@!;3&c8r z5f=Z5>6q7L4A6)*-pB>i9a&%czcxT7eEpKVwrHZfviMbbm9hl#&1>@N(uwjK<-3XU z*2=f#$tB~Y`+KKI>clkdjc@TzEA2mT^&arg7=S#07Xz>!h`B&h6U=&B&j(;W(9Q;F zi`t;9FWSmwi!YbAHoYa2Hcygwett*Z`2~-ZEmpQpl1W?NmC4)RlZo5jkn_LqD?`^@ zEpKdlQzq|tPbTenSLIzizPpn$StdmYf9A<9d<}Vf*E=#{*Gux~j>qMW?IUI2)+?p= zFMXuz&)s3`^CWwt5BD2F`Mpkl?9=YA?W|(3-%0I%?utBVySTk{TX3>G`Qtd5{4?Hf z(>q2s;t}%JhPRDi-tk-O-;%d}#N&@|$(xiPAZv}hzUB>?uxf%lwhY&~5Z5{TO6fVJ zm$ZAggQUHlE=^v+9EX=vCG{oL4deVq#lE%uDeC(J_w4t8@9%2u5S zHO&LC&O&_v@_@j zh~OipK7sRqo`U^&8s`B$*Id$GNSEd>;XLnG;P>N9?-sAO1pXn4{|vKL3wHfY+^5%ZpI*cL!F|#DgE^qhUWDDp zS^3QKYf44{juklwx2#K z`~F?t|7_TPAO6WVmB}xAO!5DI%pS?F!KW5Oduh-85u=mZ>1REXTxIj+_kPCGA z0B`~A7iUYGCEyFbfuFDx@*VK-9k8(yx?&|BSK&FU!5jV;_*s)H9oGV5KXj7pAAqTK z9VL4`@U&k7*# z@5g@jceu8t9i=TkS8IO0MOcr8kgtL1uh7>w507);H+=zo&qgoh=Qzvv(@bfB*?}!E zSEa?&meTy=R?_@K2x5umlkxq#kax`b^FHu?urH8(OzaD$&eph}zQ4!&!S@I6&)JWL z_XqdSydP~pu+JJ%hkx#udSSkQJ>6HD_XF3Yw%_63_58ZLf8VO1o?rU@;QlL}ktmf; zPgK6y;C}%4zqtB7{?z>ck;HrCYsHKI7K2(znKR02J!LHXAC?C|&8GGPk^?{Kva z1qQ}(znSZGK7gAE_W!qc?$Mf;)qS^hHD)C;F!uqLTq<`^12YT*0|*QQ$Q|UOh>9YK zz$jp`Mi5aX-lB=OsPPhQ(qtuBZPR6HD7bIyDF{hr_R-TS-0dq2;Qny~i-RjgnQeC`Vlm)!Il z;sJ^+>^VWKIW0MWd;s^JqZW9^L&^b~T7Z23J%N!2R3A|50VNOEMo$1W0dWBOgU|n_ zOfeJeIEE^Y!U2e@V9Xp_v9@q-VbDAvaH-mxwRrS+FpFh7v`I2J)S&IGC zQmm&C`;}XvvoOxAQ?~E@)ET}Gvtqw;f5-mF`$ua2(eqcizj!|}!T0*p{HWYNV}H&4 z#rxIsQ+s~N{l!1YMafCC?ti%O@4CNqe`@~Z{)b*4_`gc=|5C;OpX>O4Qux1K_*YJK znD%-=2ds5KbU<;i@f@IX0Of9@bpXYA_5*kay+Fc096%h<_-9WL@UI+^wWjonoFeW> z?TA={4V~91I|CxLM96)pMbaQ~wJOC}Q_5(aFEx>)jQ4ermV5|**2dE#Id)?;p49|HUIOQDs3efdkc6FrsIR1^Ekvy6R> zH{(7l_LKL^Pn-Mex^ewZam|msU#8g4+V5w@dLR0z>UuwPKXQL!zceG_M%De{{?z^H z`9u3B=iDLu)AQ@TpPBo89-eFdgnx7Y#Q(L5|KMNumD};wj{m9gf9L7LYT+MEg*6q% zKfQpd1EK*mIRN_X2>w;Sp%<7szzIZ%U+|U# zh*Ks9AkV}HzyVG}2l!phN9{p<2K@la15yjL4~RU#93XoFvM-39AYp(#?%Q0zagulm z4*)~t&-elR0QmtpcFP6O+wlS8oFIJxaXW05KWH$Yuc?^NIM3rG+-I(@<4K;f9lj5D zX|WHl0sGWFTkLCnbjE#SU*m&jTCtCos=f+172Jw`3-SMEn2Smpj;U-P4Ie_>zqCfEDdo?qAfitd-WfA;*()cw)^M(ciX z|3kt5m4W{r|EJXd9~-uS|LcT*^;6l@& z;KJ6!#EqQ~pp!WlAchM6=>rsBTP|Qf(DH%#d_iJb{9q!^^ZCHLa}415lBXE^I$riV;`nDczwkefeVsphB^2yO>_<11&xH4H74LUGC7+7MI>P(Ky;|Py zyq|hs=zhe0_xm>eer@bONqykX{geBHf8~f?@8{TmeCheg*spn2b$_k*HTNg}bFU9+ zQb(ZsW$c&kmvz77{)b-E;QzrM|EG)pyM`@y1pZ%Ida9|fW*wmR0_`pbxTm=$2avXG9e_MwmJWaxSn`0a(yFNkWKRHddZr#At_$Cd zeL~zDC>~%R&}f3G2ar1xPq$;!_<__2T3=vpFcZhCc2IH7bD=4K_i@bgnIpIt{=q#a zpJdz<_u(vweKZYe8zcEXoS(Q4_Jj9(-z4HbS_$Kn*jG=5dn>4~NMnU#!TYJbq~^Ez zx25|9@3-!!`?r&`Ro&0^el-7beV@fWej@hM4{qHL?yuNSPl&nVq}UJlSHFMZ{@L>@ z-XHic`+Y{wFWg_cA9cTs{l@Yc)Icc_$fZ%jL!)F;RDtIqu8#YF zKT)mDd4ce#{F}bp8BS1i0@mV|_^rSH0-N-A^M76s-UIW-JsjWhzTn>2k6a(01V_P2 z%-6A+@3;AW$#tsM*J7WXKe4Yh6!=VYevRKm-ai)m@yWH`5BAahBJYRGd9Nqu{blVJ z*V}7-qUKlo{mK0mCq}uy;=XdqqWcN|q5GAd|FYhnoYZ^$spsdgCiW-(54q;h;gIJa zI;_8Z!?5~!YdZc_445kZC;qQlQ#4hLFFIhwKRN*Gf!qr;K?f9f^Swb+2lSdiaRB-H z6W?0)1@XRMt_7AHfZ7oCVe0_sfUX6UImJG7@XL`rU`z`nH^m2>(bfa#4Fvz1+mn2N z>)zx7=mD5DLFEIP3lK-)(&Phhfe}9-P5=&TUJyE=eF6SJMt*=LrssNWV?6Io%(wOY z7WeW;_$2H5_^6ue7rtLOe`0?u?x)@d_LKM1Ln+>mhJuz-c)uRiYca3pN9+gt)ckba zyw;~(=i9`7YQUx6ujKvK{ocGA-S6(if8_qG_YwDpe-_}{J%x~ z|GT~ZH=hsK1^!>PWmqlzANJzXPks1J!hhBQ+VueB0M-G^zQDB~7#~n|0M-MQ>y6JZ zJ;Brhr3FPlko6&afVIHVGse1+RtwyiT3~1ar~T8yN4F?0anBt01b}~dt8^>oa$O@l z(Fc?~xaa}l2k485T)=(+9}x9H=L3x&us+!21=Sar7fkR6iR(#^_zmn&v7$AkeX zw|f5w@7FrNvd$;^{d65m-ak9`dyoG%?^k>{mVJJ*=2zGHEAG?tN9@-;bHA^!PaO)~ zuk7`cJ%5Lfa)0Ih#QsCBj`**8OYi9TpDzCY;jZDd9b3FVrT0*g51_BwwE*h?Q472^ z)&$}M#`*zW3%F-=Z}74%SoHv1d+LF3fZ_wn96<|YeVFTk)B|cBAU_cGK=+r27T|gS zI`r?%)C9>@od=i)6g|*u1c`6#6=FR=e1P5n`rQ-@seiL~xO;`Qe=zsHVDCWhA1Lk2 z`uSL|fPDcucT+2HE-oD&Eq=T5gJO}ZldsQZ!6p1&z27x__`R)s4|R!=crU+HjxF^4 z!2LPkp7UsPePX^eP~XQ#nkeItbw0+2bySt}Q|FJkzvZ8Y-Y>pI4+a{H`}{)hcg~-h zpRSjAKbpUM`OG?>!u#p}pa$f1{;B!F`_cV&hkg2eRBJNtSHBVsg!(aAYok51@BOb+nmYfyfPr!I=GkaQdyn z%d0MDeh@w(eZ$z}DAoh>{+#l;75C0X^u2JBn&$`hGu|tY;rQ`o_Gym!___xBO^x4L zf1~$pLGJ_m^jK2slg5JHYV1pUb@pZ#dSf7#<-+O+g3$EZg4OWuF*Rfh}*UwO!| z?h@f&_m}PXpECa!{x^gFt5&<0%HwMtu;Bp0zx#nEaRBYnR`v$d-XO1+50DPPdQN&f z%ep}J22dTq{Q$`U=nIw)fCJP#Kt2F1fP00uJ|MIJ>w(!9q&ZsF2b8&MJ&?Wta)Iyx z)U?Na0Q-lZ{e==6%RK~2+_VqiUIOkBnDGPKsxMIN)qRDyw~+5K6gt5+dhOBUGKcllh4uP zh2}@Sx77QI{Ur|^srPGsc%9!k?}t++@7FvlHGk#&!asSx*ZNibA6EN)54pDJe#ZY5 zTKgybbARcM|0jw6D+B*<0CIqv!~@U)QwMM_z;1E?`S7EEHt>J%v7`8x@3$5R{$*7M zXz`C0IL-ma`T~;!WM9x`^`nn)fYbx!1E>j9JwQBQMiXScVDt%+4}4Mm@=+fohMwzu zK=_Ydp>x5%eL&&f+bSmzR$ViI7hu9aK0sFZLHGrH!;0+uPm6zIzv2!xzr$~ufdAI~gL||+_BJ;5 zsp&iBgM0j!9-Zs!o}KRL)p@1HkIu(=iwC974?k-A`{=7Q=U08z{nqNY^7yj8hdv*8 zzx4jv=R?juXMSzGC;dvqd25mQ9E?~WIeyXi3hwI~p0PattP!10`o430 z;h(%nG2eZ?==}6jXTBfU_qx7GdY@wdNY0;Y`&08{U;pUyE1I8hZ|s-6|9Hg!*ZPV3 zsrxBbP&=yodu6?!ydUh7Z-RYs&q=x;>-_2Ut+~JQ{td$ZhRcM1?KQpX{8dW{|HS_J zZ-yC29>@4xf6ohky#V}ge&y!D_-9;Sp!>jt4(L1}Ie=;~^Kt-P(_>yo9#F0|bsl_w zqAHh8} zy>e{UFU48P8lK=Xnde8$=R9Y6dx-fh_D8io#?RQtr*a>_)cVzj;ke)C{4Mq~=U40} zr<>9Hb&XZ?Ij(B{!avw&Z$HHbV!wNRSmP`1=vvaNve$10|K|P5KMxc4&t8AHX|D6F z*hlwM?B8(t!NUK+!`h414J)@TDg1924F7T(tn!+<@IL+KU{$E%2 zP=R9=4xk*M;Q(b_P}v_aYXR)d>}yCZz)&uYX z)C006D2|zWAbkPkpt(+vIcgrjTH(wE=no(lP+YT~jSrwcNNq4Qfy@Wm8ey9gv^4^F z^^6~Ad_kKd2&2Yp`U4(YJn!!J_VxO`#&~=TpD(kn=lnj718raN-rBx24CgT5p7pz| z;S=^#-#7Nj^IN`8+;?AZ(fPBtH?eQ6zxMW*+>3E1_7~1y*7wZx_(uK)?{}XMIlp4Q zbADa#;Qe0X=eQs1`y=*yjUV}A#QyB{nc3q**iZb^mqJX+exK_i_N(TH=AYQ#aH;Tr zq40m6@c(A&e@kNj|3$_BPR0LIfBLlHpi5SkK5FGv^i*00s9Ip?0ODjL9AJn1_N$^N z2pu5mf!-eo4iJ68GwXv(Y?xRNV2yBHBPc&WPXK-8wJ#v_0Q%3!OXUM94`7{8<^t{$ zmJeXAvv*MT2*3ltMCt<62Z)z4-&4T%780)Lk%tRl;4S@ta|8Uq1YZDd)6fu!-6J;P zfBE`Zm@l#3z6Z?f``x<$*XP)<%<&WVI)_@i;VBKIQ(Z1)29}y-#@}aUx@XuJQAI zJzrVseZIFB*l+UwcAbCC``g$r>~FY4z5l}hs*ZnCfobA@i+^*fYjsap;olq}@_^D0 zz`DR(8{BY!#DDYz9Zfw@_xve7K#%hM?g@Ycq!y?cVIM#qQ2T@QCp+WSt7=K|wCU`!V@2M#WfHA2TrY2!0~AoMfzK@4pm{Q&iXabJ+VM9LFt zt`PA%x!$i{ z|6JprdB1c2i2arOU%Fw~aPfv=?FDOxmFKP;ZtL-Xy8b`$|5I<-JREfKO6w_%E$b=K z0jm#)p1@KIn4klQ2RH{1pMwLG`voi4lh1cgkn#Zc1gR&0UJ%y<=nYOSAomH8j$|#M z`T*TOHh6&7nA-;g50ECf>DS7d6!QSZv058ce*kf=%?G@HNRto11)L9169+@-2Z*U~ z0cmMv9YOH})fZHMFvbl6yT)Ro{m?K=@F@FNz zC+?&3p^r)<@mgN-q>6p@_E_hSxL^AF3g@qJzs>o@&+Mbg`HB6~alO}@t|MzcYpsvE zPqV()dOy5BV}J5~#g4khCpCZSebW4Ey&wE5AN4w)R`ZX3pXC1QFWE4xf8K^+&Gxm! zLFWknZ(17te~QtI%m2T&b2#-Crw#``?;!V3vxbuOl;!}`0;mVx7&<`o19)u^8m;)+ zk+(%pKxR(GuY3#A6kFJeRKY{#|N!HvCp1< z;%{Et3-&ATr}s;iykB`>?el~8D?Sh_M(Ta={?z=zKK(xQ`jGc)?l~5rO^@gO=zh}t zHYoP5SL`?b&lmom3;u6g68*pX4E`??{>7P0BVA0f$j?&s|SqxfT#&p9^kdY=mDknTzUf(qf!$@4 zM|W)zT({Od zHxl1lyzd6{Ub6%4WoQ`l;ofl{4n*81=7W9oQjJg5`dr^n>?^;5^Q-^TdS9&Rt@rQ= z?AJO!u|M|nbe+G^`;UwDJ;$N}k@pk#RT~=T{dJ8G*w=g_zZ}v0n0LxSv)*ss&pIFV z`OonFgDdveU!=9a;Q#Ev{}R~$bMf)NYuI$N;{S!hzsA)*;2J$%CmldO*Zlz20mBEV z9#isw&;sEAw+08uJb=26e0}Z_Q1SrbUs^!s0V6d*`vA?S=nL?ELG+YFO#mLiz2tJe zaMc902GwQpf$N9(g4Q2^-PY1S zjN`j~O?-y=2HZY0J@b%BJRj_*wiof9+@|LF4d17q&-J}n%j5lflJkRo#zIwqxDheOYG0m`{4cR@e%e@?=S0n)cc|O57!+n$oKIk?!)^t_Ore>_VqOOUoo5a zd!3KCf7blqrUzeL`uvmkZ-Dm``!8BQtiMpbzuVRb{|61X-QVN?RQjLt|4lC!{x1;z zuPnY59nc&g^8j4~co=^WhsB6aFEUeKs2VjqYSQ~WIYsdqV1C-w2>iPJy z#C;p{v$jv%r>9TYXMd$!&$E%Wd{N^g-zUdP?AJOU_wJ3}UTgj6eRV&t!uySV`g|hi z2m8+Zl>e3ccnkYc>(6_6kJS4j_Iv+X^M1`W=9_ZQ%Khp0QS8rkKI8abC;YFwK=?mT z_&=-Tp8~*i_}@8fdfBPNitPuwmdg0T0jvWwT43!7azB7_0knYZ2@-#+J^&72A5gBj zdjiD&Tn|t@fCC(>-az{R_XVULs8|wuVETZ{12hMplf40~HJ_ykio1T2+CcaKV=snBv$bUSYmj}$J8AQ*9gwRXUoj- ziS^_8J-Ys+c%QhJ_E~#+9v<+0@_lNp%|1TF{SmF79-qK|-Or1- zFD<98?bCHd?kdV5_MdwSJApOW`i>{rdN^!b%upOW)e>?;R#ENWuExj%V-Xnw^0 zb&CD#whRB~t{x6LQ}};lkN?x-f77tyeBqxS>MMnR@&I&zRtvmA93b)la{%%H`ZM7G zsRh6TyiZW!dFFfc1St2LsR;`I?hBal0cEcMdIK6C;9NlW5fkS`b7GAN^O1cfW_pB5 z%t}pweznpkr1>o_U~N$G&b>mZ3y>3t57d4E<;T_r&<7$fuuh0CaBhH4kROmwurH`S z0iFoYwRM^U3p_vAF z_Xyzw&;#fd*4%Y%KyfVd0eJ09pFpW`aeuMa4`koqs2_;_K{$bZL5a1scKGRnN%qN1 zjE?w(qV@$Ka*b@!F`+O8~Yj) z*6;2ai-VY}o>Gf;!{u2Aw4eQQV@6S2H|JD-!m&p2`?TXWj*8jWn ztHS?FHw_1zyFzsoaVqhuSv(-~0Imr<03T4bfY1Zs0OSGE0?qH_=iM84hw=f}1d|6) z7Zg8)C%QLCz99As$vuO(hY-2I3EV>jomp$jy>}RU$}%@|%^>%hVD3_*=AQH9uV={vnfk9B$+3-Mm&<0Q97*Nb@1{s=xV=lF{AoG<-6sqwLASL*x0 z_e;zd=2c@c->+JqxW3}PdwVqgzGtVdhwt6tIvy&0n>C^M1wswcFPXYlZ!_ z=L!F3uNn?KL-@b1^Z%*%|0Mh;2beDpfCs2n+tvfPN2_&Rt>t1bC)WhU1Kb;|T+lfo z{ejsR(AEW051=<7bAi}5g!>6+E?^#@e4yk4!hWj>cn^_M%Ocj*oPZeEtFgFm-`i?^&iAvoH{w3)JhRSMdwk>v zjQxoHaQ@uWCwYIy{^b3NC)WGnnTj*s(_8tc<9=xV=KaJd_4&?;|I<7Er`G?(|2MvP z<8Z)p4{%OJFNJ$5t`HAE3lQ&e?scv70OexTVp(GM>SjS?H zv!}535g^aaeMKu5piV%1)ZSuPtrOq};L*fY_8L=+HQwl#PhOz->-!Q5qh50yF}n5^ zB!(y9n%90x?~m6ww(Dv* zRpUe7hx0qv7v9D9eUIMM_!-l8m!52}@BRDUrt3g`hJE|V_hVg8?%|nQKfPW@zgBq9 z9v|Yqdcw**JQVlE3y*kZ(TdQGy{DIAN8Qtp*e`CGeSWppulbjI`)s&OF(2Nq*l*5{ z=68YOKJkCs+F|Xv>is=y^{`^gfy1qDApZCDKl5>bo#6k*z`w?o9_pG0hzHb~AbKF2 zOvW{GUm!la-6w$c0m=i8yhYeY3zX5{X&yjL;EtjP(I43I0PBJF0g5T?8Ve{tAs*6}}W{C`yVKSlU|`q>94pDMm}#0O9lxGH#naxq^U@c`>HFPg0d z%-laTdILfauomFDVDNzK4G12P>jWHE&;yuf>?5wYlY59}eb9Rc_`YKB05k#OnDPN? zR*g28b;6&*4}?BQ&CBbEqz#mMp&p5wSoRKtR_L5sadz`>1ur1}CN~f+Pm8$h$J7tz zc{HDh*JNzx{oM0BKiirFJVZW-{dn5k-tiv4rL_+}X6N|ezMPl$OZYfChk!d25F?=g6L`-oLFYhZPn7`~Lp_iqDIx|2_I;;r|BV|19DELXEG+7Jh}VH4nHv z@_^6++#3WBzz2}4r4M*X=?(I|LxKlbrq#vnD_;(D;C&2Sk4W{iCfW_+Z&b z+ z17uAA9zaci`jC78xxg`v9x$se@aE&K2T*rPUGM~Q0epb?Dmkm`1Mpb5fZ`hSy!HxE zE37#|(FT4h>I5f$ti(+?!70iMW;BAbzL5KnzzdQafW@pOwm!jr0dAhYp?QQqwEwRd z4}ZevxHd^X|Lc6;%y;_xBi5JW)3I3B1M}4Kbo`3sM_|K(#ougClhKjvp=KEfm458eaw*7UMxx5j$mA6;*E@!q)C zcrY&FNnX24{)EN{-)9|Y*CJziKV+ z37{q@A7DLDKA`pnvPLNNK=C>5A;!Kf&I9ZN${cV_K=*WUe~@^iqA4+<~% z0(gP+0qhjj#eGjp;wm~pt}*3w|0J-K~3hv(@2dp;kXSFPo;(qSq z$$e#K>3!snv96c-G@|!uzPS!nYklGX!TXK>^Thun=ZE`O{BIHdH+K9_i~mQy^vLk6 zYo0y)y;Fb8>!w#~?}UT2f6_|rp}JE0D6hI;)v#LkQ(E=B)x+wG#j!3~GpxBxTI&@W z|0~sVb+vrP^W`(H)%CefIl~L(FK&>pc#-VIvX_W2+$4YTa{2h1b-izq{j~VPE0r(Y zDt~di{KXF0tK~E9kliJJ@fz9P(vDuMdGtDI2R|!+aj)jsy_#$H$=)b`@g~^=@)-{( z7k#kUv2WEJeVgX%JG9R5o$?z$uQ~iK*}LU8eo@8ZGr!hS00gb7~ zlW~3K8yII{`5xKp%lLC0j(DxOysit^=kPm)WnC{{H(kGH+@@=(>&bOJRM(g5%=JFx zWxDn+RsBx$V1wqvh8r|DUZ8k>z2=JM3-e~J<`45|%@xYuHLsXo%rnn7=AGu>%JWxh zPfyLub60t9j{~*7f5n*x4lC5>x8jTghXYPu;hNuy(^d>WvGFH|r#<_}hg)8EOV9tO z$p0Vv>SM#!muyw-WZiJYC92Sxp%Ab_uGWQ+oj{ZTG+co_XNCCxVuaC z57g&-w_^7{R1W_-;qCRZdxWujWpB{BpEoLY->-Q6X2t3Uv=`W0q~AR#d#hsM+ZChV zq4@Yt*}G)#R&2$7QL*(s%BkNgd!KUh_p6roOVaSuJ}B+}L#pBZa;Ln)fWP|AUk#6c?eXC=U--=M@E0F0_9Z>aKKJ{do3StJ_tw6mzrXs3?UAoNG8B7s zc;pWr9UjI0FzoAk#Qsg#H}r`8TWsg>=pPl^smGoFZdb8?ziZf)_D%hceJkvb^@x4z zv0>M@KW}?XkB|L{>`y;Gd>;F=3HBfKzj6C>{SW((X^-pov?uht>@S`e9>@OEY3jKp9|y-$kBwuDW9RtC&ny3)G!7ac#)2$DT)B zkDXXvqquIqeq2YcXI|U9)_Lu7E@)mbKWxky=8qmdkC;#G{Ni`!-=(`uS{>K3H zpFRT1cmz8Bm+{5#r@rh*pyPkpPk}xF%XkDj{+IE^@29@(N1)?>*-wE!0Lyp;I{ugO z#qX!S>_?#Ef7wrgJ^;&j1Umkg@x||_zU)WfGa7;87Nhphcj~);l=c7b>GSut{jI(e zo2vOjulv7ojJ#)WA8Y@<s%d<{pXqgPm)#@V z+sE3!Z+VaT1DbO$l&#)Rki?18vH-1*R5a7W9{GFM_{o=Ky!b8#s8g80{&AHbrGK*xW_|DsL4{>6(v0{b!k z@6x&bZ`pi5JN1H||1bJX><6$IBcQo|f$YD@rutcoV_e90>SXSg(c3rQ&l7sV!3+70 z{+>PpOML`1_b-%z{~yZ0%bd1U&R6Ggy=IV*pnlmx&JiT2HC;W*q;3E z{#ktl`Uvz9=p)cappQTwfj$C#1o{Z{5$Ge(N1%^DAAvpseFXXl^bzPI&_|$;Kp%lV z0(}Jf2=o!?BhW{nk3b)RJ_3CN`Uvz9=p)cappQTwfj$C#1o{Z{5$Ge(N1%^DAAvps zeFXXl^bzPI&_|$;Kp%lV0(}Jf2=o!?BhW{nk3b)RJ_3CN`Uvz9=p)cappU@*H3Hhp z=qF_BW$bMI5us?9$|9fG>+~2jt zM?iD`BH3S00~B7fR8Cii+VB6nR4!IO&P5vm;bgn)FJ)8xEZTuS$uHK)bo@WbG3zg1 zej^~9bo}@Ge=Wa>-_O`1(DC2z|Jk9id`F<;zvF-T&i;Pp9seEw9R^QLubRpyR*e zfBDY-e&!wj#{c?06Z!}&#}N?z&zCLd^*;~m{8ul>Yv0egkAUX>nX>;Xo9bt&jF?Vl z+Vy|m(rfRL?U3Cug}p|vxn8zzPmF~==TO;;WPI*4-@h-v<}RJX?Xr_)Pu&yaGuLP6 zJC2pTTE?~Bm&awAW8)ldmOW#xbDrxtz4rte<3G)KFmA7seL(hI*;GGsegD4noK9w1 z{C{7s{U5UL#3R4YWq+gRcgZ&HiSyOxoGtsJjL)6xdzZp<%%lGzV_rRNPmIr8pP}z~ zf$YD__}-;3K64!-=kRUWd2^lfT+ivfFO@MK?B_Apckvwa9s8fMAIhfsnd|%arRQ`q z)8c=s^B#NASM>MhJ#oJJobzOVH1_#R`Op8QgJwEssQXZ_aV~2-tU+BEj zdtV{@!PsZ+(LWb@Y-x0J+{65K8-`izXo9ue)cB*(M^7&^L;OmU&sF*aum959shfI z^2XP7{4admJN`TV7e0ITjdynZFZ?=m{CE5>eD>@c@9g+r_;u*`@AzN%?AbTo+3~;d z>(KGv@xSodvv0h!i7&locsFp z=a>2T3~!oqe)jm=f_VLr(txXzlz`W`LRdl$B#edoYA56U-DiW zUwqj2o^n4Avw!;YhchVijnC|X>;LH^F#QP3H^2Jx_fHT7|M6iA_t&5Q2NKO9CjbBd literal 0 HcmV?d00001 diff --git a/VirtualDriverControl/scripts/render-audio-interop.mjs b/VirtualDriverControl/scripts/render-audio-interop.mjs new file mode 100644 index 00000000..30d30333 --- /dev/null +++ b/VirtualDriverControl/scripts/render-audio-interop.mjs @@ -0,0 +1,19 @@ +// Dev-only helper: renders the AudioService C# interop to a file so it can be +// compile-tested with Add-Type outside Electron. +import { readFileSync, writeFileSync, mkdirSync } from 'fs' +import { join, dirname } from 'path' +import { fileURLToPath } from 'url' + +const here = dirname(fileURLToPath(import.meta.url)) +const source = readFileSync(join(here, '..', 'src', 'main', 'services', 'audio-service.ts'), 'utf8').replace(/\r\n/g, '\n') + +const start = source.indexOf('const CORE_AUDIO_CSHARP = `') + 'const CORE_AUDIO_CSHARP = `'.length +const end = source.indexOf('`.trim()', start) +const template = source.slice(start, end) +// eslint-disable-next-line no-new-func +const rendered = new Function(`return \`${template}\``)().trim() + +const outDir = join(here, '..', 'out-ps-check') +mkdirSync(outDir, { recursive: true }) +writeFileSync(join(outDir, 'core-audio-interop.cs'), rendered) +console.log('rendered to', join(outDir, 'core-audio-interop.cs')) diff --git a/VirtualDriverControl/scripts/render-ps-scripts.mjs b/VirtualDriverControl/scripts/render-ps-scripts.mjs new file mode 100644 index 00000000..9be71b70 --- /dev/null +++ b/VirtualDriverControl/scripts/render-ps-scripts.mjs @@ -0,0 +1,81 @@ +// Dev-only helper: renders the InstallerService PowerShell templates to temp +// files so their syntax can be validated without running Electron. +import { readFileSync, writeFileSync, mkdirSync } from 'fs' +import { join, dirname } from 'path' +import { fileURLToPath } from 'url' + +const here = dirname(fileURLToPath(import.meta.url)) +const source = readFileSync(join(here, '..', 'src', 'main', 'services', 'installer-service.ts'), 'utf8').replace(/\r\n/g, '\n') + +// Driver specs mirrored from the service (keep in sync when validating). +const SPECS = { + display: { + title: 'Virtual Display Driver', + infName: 'MttVDD.inf', + signedBinary: 'MttVDD.dll', + hardwareId: 'Root\\MttVDD', + hardwareIdPattern: '^Root\\\\MttVDD$', + className: 'Display', + classGuid: '4D36E968-E325-11CE-BFC1-08002BE10318', + copyToBaseDir: true, + preserveFiles: ['vdd_settings.xml'], + maxInstances: 1 + }, + audio: { + title: 'Virtual Audio Driver', + infName: 'VirtualAudioDriver.inf', + signedBinary: 'VirtualAudioDriver.sys', + hardwareId: 'Root\\VirtualAudioDriver', + hardwareIdPattern: '^Root\\\\VirtualAudioDriver$', + className: 'MEDIA', + classGuid: '4D36E96C-E325-11CE-BFC1-08002BE10318', + copyToBaseDir: false, + preserveFiles: [], + maxInstances: 4 + } +} + +function extractTemplate(afterMarker, openMarker = 'return `') { + const start = source.indexOf(afterMarker) + if (start === -1) throw new Error(`marker not found: ${afterMarker}`) + const open = source.indexOf(openMarker, start) + openMarker.length + const close = source.indexOf('`\n }', open) + return source.slice(open, close) +} + +function render(template, vars) { + const cleaned = template.replaceAll('this.getBaseDir()', 'getBaseDir()') + const keys = Object.keys(vars) + // eslint-disable-next-line no-new-func + return new Function(...keys, `return \`${cleaned}\``)(...keys.map((k) => vars[k])) +} + +const outDir = join(here, '..', 'out-ps-check') +mkdirSync(outDir, { recursive: true }) + +const installTemplate = extractTemplate('private buildInstallScript(') +const copyBlockTemplate = extractTemplate('const copyBlock = spec.copyToBaseDir', '? `') +const uninstallTemplate = extractTemplate('private buildUninstallScript(') +const restartTemplate = extractTemplate('private buildRestartScript(') +const instancesTemplate = extractTemplate('private buildSetInstancesScript(') + +for (const [id, spec] of Object.entries(SPECS)) { + const copyBlock = spec.copyToBaseDir + ? render(copyBlockTemplate.slice(0, copyBlockTemplate.indexOf('`\n : ')), { + spec, + getBaseDir: () => 'C:\\VirtualDisplayDriver' + }) + : '' + const nefconPath = 'C:\\Tools\\nefcon\\nefconc.exe' + const installFlags = spec.maxInstances === 1 ? '--no-duplicates --remove-duplicates' : '--no-duplicates' + const common = { spec, nefconPath, getBaseDir: () => 'C:\\VirtualDisplayDriver' } + writeFileSync( + join(outDir, `install-${id}.ps1`), + render(installTemplate, { ...common, packageDir: 'C:\\Temp\\pkg', instances: 2, copyBlock, installFlags }) + ) + writeFileSync(join(outDir, `uninstall-${id}.ps1`), render(uninstallTemplate, common)) + writeFileSync(join(outDir, `restart-${id}.ps1`), render(restartTemplate, common)) + writeFileSync(join(outDir, `instances-${id}.ps1`), render(instancesTemplate, { ...common, target: 2 })) +} + +console.log('rendered to', outDir) diff --git a/VirtualDriverControl/src/main/index.ts b/VirtualDriverControl/src/main/index.ts new file mode 100644 index 00000000..3fa72b3b --- /dev/null +++ b/VirtualDriverControl/src/main/index.ts @@ -0,0 +1,238 @@ +import { execFile } from 'child_process' +import { app, BrowserWindow, desktopCapturer, nativeImage, nativeTheme, session, shell } from 'electron' +import { writeFileSync } from 'fs' +import os from 'os' +import { join } from 'path' +import { promisify } from 'util' +import { registerIpc } from './ipc' +import { AudioService } from './services/audio-service' +import { DisplayService } from './services/display-service' +import { DriverService } from './services/driver-service' +import { InstallerService } from './services/installer-service' +import { LogService } from './services/log-service' +import { PipeClient } from './services/pipe-client' +import { PrefsService } from './services/prefs-service' +import { SettingsService } from './services/settings-service' + +const HEARTBEAT_INTERVAL_MS = 5_000 +const execFileAsync = promisify(execFile) + +const RESOURCES_DIR = join(__dirname, '../../resources') + +// Taskbar/window icon mirrors driver health: normal when the pipe answers, +// yellow when installed but not responding, red when not installed. +const STATUS_ICONS: Record = { + online: join(RESOURCES_DIR, 'VirtualDisplayDriver.ico'), + 'installed-offline': join(RESOURCES_DIR, 'VDD_Yellow.ico'), + 'not-installed': join(RESOURCES_DIR, 'VDD_Red.ico'), + unknown: join(RESOURCES_DIR, 'VDD_Red.ico') +} + +let currentIconLevel = '' + +function updateStatusIcon(level: string): void { + if (!mainWindow || mainWindow.isDestroyed() || level === currentIconLevel) return + const icon = nativeImage.createFromPath(STATUS_ICONS[level] ?? STATUS_ICONS.unknown) + mainWindow.setIcon(icon) + currentIconLevel = level +} + +// Mica needs Windows 11 22H2+; everywhere else we fall back to a solid Fluent base. +const supportsMica = process.platform === 'win32' && Number(os.release().split('.')[2] ?? 0) >= 22621 + +/** + * Relaunches the app elevated through a single UAC prompt. Returns false when + * the prompt is declined. A .cmd launcher preserves the dev-server URL so the + * elevated instance still gets HMR during development. + */ +async function relaunchElevated(): Promise { + const lines = ['@echo off', `cd /d "${process.cwd()}"`] + for (const key of ['ELECTRON_RENDERER_URL', 'NODE_ENV']) { + const value = process.env[key] + if (value) lines.push(`set "${key}=${value}"`) + } + const args = process.argv + .slice(1) + .map((a) => `"${a}"`) + .join(' ') + lines.push(`start "" "${process.execPath}" ${args}`) + + const cmdPath = join(app.getPath('temp'), `vdd-elevate-${Date.now()}.cmd`) + writeFileSync(cmdPath, lines.join('\r\n'), 'utf8') + try { + await execFileAsync( + 'powershell.exe', + ['-NoProfile', '-Command', `Start-Process -FilePath '${cmdPath}' -Verb RunAs -WindowStyle Hidden`], + { windowsHide: true, timeout: 120_000 } + ) + return true + } catch { + // UAC declined + return false + } +} + +let mainWindow: BrowserWindow | null = null +let heartbeatBusy = false + +const prefs = new PrefsService() +const pipe = new PipeClient() +const settings = new SettingsService(() => prefs.getBaseDir()) +const driver = new DriverService(pipe, { getBaseDir: () => prefs.getBaseDir() }) +const logs = new LogService(pipe, () => prefs.getBaseDir()) +const installer = new InstallerService( + () => prefs.getBaseDir(), + () => driver.isAdmin(), + (progress) => send('push:install-progress', progress) +) +const audio = new AudioService() +const displays = new DisplayService() + +function send(channel: string, payload: unknown): void { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send(channel, payload) + } +} + +function createWindow(): void { + mainWindow = new BrowserWindow({ + width: 1320, + height: 860, + minWidth: 1000, + minHeight: 640, + frame: false, + show: false, + ...(supportsMica ? { backgroundMaterial: 'mica' as const } : { backgroundColor: '#202020' }), + icon: join(RESOURCES_DIR, 'VirtualDisplayDriver.ico'), + webPreferences: { + preload: join(__dirname, '../preload/index.js'), + sandbox: true, + contextIsolation: true, + nodeIntegration: false, + webSecurity: true, + additionalArguments: [`--vdd-backdrop=${supportsMica ? 'mica' : 'solid'}`] + } + }) + + mainWindow.on('ready-to-show', () => { + // Re-assert the material - it can fail to apply on frameless windows + // when only passed through the constructor. + if (supportsMica) mainWindow?.setBackgroundMaterial('mica') + mainWindow?.show() + }) + mainWindow.on('maximize', () => send('push:maximized', true)) + mainWindow.on('unmaximize', () => send('push:maximized', false)) + mainWindow.on('closed', () => { + mainWindow = null + }) + + // All external navigation goes through the system browser. + mainWindow.webContents.setWindowOpenHandler(({ url }) => { + if (/^https:\/\//.test(url)) void shell.openExternal(url) + return { action: 'deny' } + }) + mainWindow.webContents.on('will-navigate', (event, url) => { + if (!url.startsWith('http://localhost') && !url.startsWith('file://')) event.preventDefault() + }) + + if (process.env.ELECTRON_RENDERER_URL) { + void mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL) + } else { + void mainWindow.loadFile(join(__dirname, '../renderer/index.html')) + } +} + +async function heartbeat(): Promise { + if (heartbeatBusy) return + heartbeatBusy = true + try { + const status = await driver.status() + updateStatusIcon(status.level) + send('push:status', status) + } finally { + heartbeatBusy = false + } +} + +const gotLock = app.requestSingleInstanceLock() +if (!gotLock) { + app.quit() +} else { + app.on('second-instance', () => { + if (mainWindow) { + if (mainWindow.isMinimized()) mainWindow.restore() + mainWindow.focus() + } + }) + + app.whenReady().then(async () => { + // Ask for elevation up front so the driver folder, VDDPATH registry and + // device operations all work without per-operation prompts. Declining + // keeps the app fully usable as a standard user. + if (!(await driver.isAdmin())) { + app.releaseSingleInstanceLock() + if (await relaunchElevated()) { + if (app.isPackaged) { + app.exit(0) + } + // In dev the parent must stay alive (windowless) so the Vite dev + // server keeps running for the elevated instance. + return + } + app.requestSingleInstanceLock() + } + + // Mica's tint follows nativeTheme - align it with the saved app theme. + nativeTheme.themeSource = prefs.get().theme + + registerIpc({ pipe, settings, driver, logs, prefs, installer, audio, displays }, () => mainWindow) + + // The driver reads vdd_settings.xml from its VDDPATH registry value - + // follow it so the app always edits the file the driver actually loads, + // and seed the folder with defaults when it does not exist yet. + void prefs.syncBaseDirWithDriver().then(() => settings.ensureDefaults()) + + // Microphone capture powers the audio router; everything else stays denied. + session.defaultSession.setPermissionRequestHandler((_wc, permission, callback) => { + callback(permission === 'media') + }) + // getDisplayMedia with system audio loopback ("System audio" route source). + session.defaultSession.setDisplayMediaRequestHandler((_request, callback) => { + desktopCapturer + .getSources({ types: ['screen'] }) + .then((sources) => callback({ video: sources[0], audio: 'loopback' })) + .catch(() => callback({})) + }) + + displays.watch(() => { + void displays.layout().then((layout) => send('push:displays', layout)) + }) + + logs.on('events', (events) => send('push:logs', events)) + pipe.on('result', (result) => + send('push:pipe-activity', { + command: result.command, + ok: result.ok, + durationMs: result.durationMs, + at: Date.now() + }) + ) + + logs.start() + logs.appInfo('Virtual Driver Control started') + + createWindow() + + void heartbeat() + setInterval(() => void heartbeat(), HEARTBEAT_INTERVAL_MS) + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) createWindow() + }) + }) + + app.on('window-all-closed', () => { + logs.stop() + app.quit() + }) +} diff --git a/VirtualDriverControl/src/main/ipc.ts b/VirtualDriverControl/src/main/ipc.ts new file mode 100644 index 00000000..b8dcf0d7 --- /dev/null +++ b/VirtualDriverControl/src/main/ipc.ts @@ -0,0 +1,243 @@ +import { BrowserWindow, ipcMain, nativeTheme, shell } from 'electron' +import type { AppPreferences, ManagedDriverId, PipeToggleCommand, VddSettings } from '@shared/types' +import type { AudioService } from './services/audio-service' +import type { DisplayService } from './services/display-service' +import type { DriverService } from './services/driver-service' +import type { InstallerService } from './services/installer-service' +import type { LogService } from './services/log-service' +import type { PipeClient } from './services/pipe-client' +import type { PrefsService } from './services/prefs-service' +import type { SettingsService } from './services/settings-service' + +const TOGGLE_COMMANDS: ReadonlySet = new Set([ + 'HDRPLUS', + 'SDR10', + 'CUSTOMEDID', + 'PREVENTSPOOF', + 'CEAOVERRIDE', + 'HARDWARECURSOR', + 'LOGGING', + 'LOG_DEBUG' +]) + +const QUERY_COMMANDS: ReadonlySet = new Set(['D3DDEVICEGPU', 'IDDCXVERSION', 'GETASSIGNEDGPU', 'GETALLGPUS', 'GETSETTINGS', 'PING']) + +/** Raw console commands: known verbs only, conservative charset for arguments. */ +const RAW_COMMAND_PATTERN = /^[A-Za-z0-9_]+(?: [A-Za-z0-9_." -]{1,100})?$/ + +export interface IpcServices { + pipe: PipeClient + settings: SettingsService + driver: DriverService + logs: LogService + prefs: PrefsService + installer: InstallerService + audio: AudioService + displays: DisplayService +} + +function assertDriverId(value: unknown): asserts value is ManagedDriverId { + if (value !== 'display' && value !== 'audio') throw new Error('Unknown driver id') +} + +export function registerIpc(services: IpcServices, getWindow: () => BrowserWindow | null): void { + const { pipe, settings, driver, logs, prefs, installer, audio, displays } = services + + // --- Pipe ----------------------------------------------------------------- + ipcMain.handle('pipe:ping', () => pipe.ping()) + + ipcMain.handle('pipe:set-display-count', (_e, count: unknown) => { + const n = Number(count) + if (!Number.isFinite(n) || n < 0 || n > 16) throw new Error('Display count must be between 0 and 16') + return pipe.setDisplayCount(n) + }) + + ipcMain.handle('pipe:toggle', (_e, name: unknown, value: unknown) => { + if (typeof name !== 'string' || !TOGGLE_COMMANDS.has(name)) throw new Error('Unknown toggle command') + return pipe.setToggle(name as PipeToggleCommand, value === true) + }) + + ipcMain.handle('pipe:set-gpu', (_e, name: unknown) => { + if (typeof name !== 'string' || name.trim().length === 0) throw new Error('GPU name required') + return pipe.setGpu(name.trim()) + }) + + ipcMain.handle('pipe:query', (_e, command: unknown) => { + if (typeof command !== 'string' || !QUERY_COMMANDS.has(command)) throw new Error('Unknown query command') + return pipe.send(command) + }) + + ipcMain.handle('pipe:send-raw', (_e, command: unknown) => { + if (typeof command !== 'string') throw new Error('Command must be a string') + const trimmed = command.trim() + if (trimmed.length === 0 || trimmed.length > 127) throw new Error('Command must be 1-127 characters') + if (!RAW_COMMAND_PATTERN.test(trimmed)) throw new Error('Command contains unsupported characters') + return pipe.send(trimmed) + }) + + ipcMain.handle('pipe:get-driver-settings', () => pipe.getDriverSettings()) + + // --- vdd_settings.xml ------------------------------------------------------- + ipcMain.handle('settings:load', () => settings.load()) + + ipcMain.handle('settings:save', (_e, value: unknown) => { + assertSettingsShape(value) + return settings.save(value) + }) + + ipcMain.handle('settings:preview', (_e, value: unknown) => { + assertSettingsShape(value) + return settings.serialize(value) + }) + + ipcMain.handle('settings:raw', () => settings.rawXml()) + ipcMain.handle('settings:backups', () => settings.listBackups()) + + ipcMain.handle('settings:restore', (_e, fileName: unknown) => { + if (typeof fileName !== 'string') throw new Error('Backup file name required') + return settings.restoreBackup(fileName) + }) + + ipcMain.handle('settings:save-monitor-profile', (_e, xml: unknown, bytes: unknown) => { + if (typeof xml !== 'string' || !xml.includes('')) throw new Error('Invalid monitor profile XML') + const edidBytes = bytes instanceof Uint8Array ? bytes : bytes instanceof ArrayBuffer ? new Uint8Array(bytes) : undefined + return settings.saveMonitorProfile(xml, edidBytes) + }) + + // --- Driver / system -------------------------------------------------------- + ipcMain.handle('driver:status', (_e, force: unknown) => driver.status(force === true)) + ipcMain.handle('driver:gpus', () => driver.gpus()) + ipcMain.handle('driver:iddcx-version', () => driver.iddcxVersion()) + ipcMain.handle('system:info', () => driver.systemInfo()) + + // --- Driver lifecycle (download / install / uninstall / restart) ------------- + ipcMain.handle('installer:latest-release', (_e, driverId: unknown) => { + assertDriverId(driverId) + return installer.latestRelease(driverId) + }) + ipcMain.handle('installer:installed-tag', (_e, driverId: unknown) => { + assertDriverId(driverId) + return installer.installedReleaseTag(driverId) + }) + ipcMain.handle('installer:device-state', (_e, driverId: unknown) => { + assertDriverId(driverId) + return installer.deviceState(driverId) + }) + ipcMain.handle('installer:install', (_e, driverId: unknown, instances: unknown) => { + assertDriverId(driverId) + const n = instances === undefined ? 1 : Number(instances) + if (!Number.isFinite(n) || n < 1 || n > 4) throw new Error('Instance count must be between 1 and 4') + return installer.downloadAndInstall(driverId, n) + }) + ipcMain.handle('installer:uninstall', (_e, driverId: unknown) => { + assertDriverId(driverId) + return installer.uninstall(driverId) + }) + ipcMain.handle('installer:restart-device', (_e, driverId: unknown) => { + assertDriverId(driverId) + return installer.restartDevice(driverId) + }) + ipcMain.handle('installer:set-instances', (_e, driverId: unknown, count: unknown) => { + assertDriverId(driverId) + const n = Number(count) + if (!Number.isFinite(n) || n < 1 || n > 4) throw new Error('Instance count must be between 1 and 4') + return installer.setInstances(driverId, n) + }) + ipcMain.handle('installer:test-signing', () => installer.testSigningEnabled()) + ipcMain.handle('installer:set-test-signing', (_e, enabled: unknown) => installer.setTestSigning(enabled === true)) + + // --- Windows audio endpoints --------------------------------------------------- + ipcMain.handle('audio:endpoints', () => audio.listEndpoints()) + ipcMain.handle('audio:set-default', (_e, id: unknown) => { + if (typeof id !== 'string') throw new Error('Endpoint id required') + return audio.setDefaultEndpoint(id) + }) + ipcMain.handle('audio:set-volume', (_e, id: unknown, volume: unknown) => { + if (typeof id !== 'string') throw new Error('Endpoint id required') + const v = Number(volume) + if (!Number.isFinite(v) || v < 0 || v > 1) throw new Error('Volume must be 0..1') + return audio.setVolume(id, v) + }) + ipcMain.handle('audio:set-mute', (_e, id: unknown, muted: unknown) => { + if (typeof id !== 'string') throw new Error('Endpoint id required') + return audio.setMute(id, muted === true) + }) + + // --- Display layout -------------------------------------------------------------- + ipcMain.handle('system:displays', () => displays.layout()) + + // --- Logs -------------------------------------------------------------------- + ipcMain.handle('logs:recent', () => logs.recent()) + + // --- Preferences -------------------------------------------------------------- + ipcMain.handle('prefs:get', () => prefs.get()) + ipcMain.handle('prefs:set', (_e, patch: unknown) => { + if (typeof patch !== 'object' || patch === null) throw new Error('Invalid preferences') + // baseDir changes must go through prefs:set-base-dir so the driver's + // VDDPATH registry value always stays in sync. + const rest = { ...(patch as Partial) } + delete rest.baseDir + const next = prefs.set(rest) + // Keep the Mica backdrop tint in step with the in-app theme. + nativeTheme.themeSource = next.theme + return next + }) + ipcMain.handle('prefs:set-base-dir', async (_e, baseDir: unknown) => { + if (typeof baseDir !== 'string' || baseDir.trim().length === 0) throw new Error('Path required') + const result = await prefs.setBaseDir(baseDir) + // Seed the new location with defaults so the driver finds a config there. + if (result.ok) await settings.ensureDefaults() + return result + }) + + // --- Shell ---------------------------------------------------------------------- + ipcMain.handle('shell:open-external', (_e, url: unknown) => { + if (typeof url !== 'string' || !/^https:\/\//.test(url)) throw new Error('Only https links may be opened') + return shell.openExternal(url) + }) + + ipcMain.handle('shell:open-path', (_e, which: unknown) => { + const base = prefs.getBaseDir() + const targets: Record = { + base, + logs: `${base}\\Logs`, + backups: `${base}\\Backups`, + edid: `${base}\\EDID` + } + const target = typeof which === 'string' ? targets[which] : undefined + if (!target) throw new Error('Unknown folder') + return shell.openPath(target) + }) + + // --- Window controls -------------------------------------------------------------- + ipcMain.on('window:minimize', () => getWindow()?.minimize()) + ipcMain.on('window:maximize-toggle', () => { + const win = getWindow() + if (!win) return + if (win.isMaximized()) win.unmaximize() + else win.maximize() + }) + ipcMain.on('window:close', () => getWindow()?.close()) +} + +function assertSettingsShape(value: unknown): asserts value is VddSettings { + if (typeof value !== 'object' || value === null) throw new Error('Settings payload must be an object') + const v = value as Record + for (const key of [ + 'monitors', + 'gpu', + 'global', + 'resolutions', + 'logging', + 'colour', + 'cursor', + 'edid', + 'edidIntegration', + 'hdrAdvanced', + 'autoResolutions', + 'colorAdvanced' + ]) { + if (!(key in v)) throw new Error(`Settings payload missing section: ${key}`) + } + if (!Array.isArray(v.resolutions)) throw new Error('resolutions must be an array') +} diff --git a/VirtualDriverControl/src/main/services/audio-service.ts b/VirtualDriverControl/src/main/services/audio-service.ts new file mode 100644 index 00000000..5ece58a3 --- /dev/null +++ b/VirtualDriverControl/src/main/services/audio-service.ts @@ -0,0 +1,309 @@ +import { execFile } from 'child_process' +import { app } from 'electron' +import { promises as fs } from 'fs' +import { join } from 'path' +import { promisify } from 'util' +import type { AudioEndpoint } from '@shared/types' + +const execFileAsync = promisify(execFile) + +/** MMDevice endpoint id, e.g. {0.0.0.00000000}.{c2f56a7e-...}. */ +export const ENDPOINT_ID_PATTERN = /^\{0\.0\.[01]\.00000000\}\.\{[0-9a-fA-F-]{36}\}$/ + +/** + * Core Audio interop (C# 5 compatible for Windows PowerShell's compiler). + * Covers endpoint enumeration, default-device switching (IPolicyConfig), + * master volume and mute (IAudioEndpointVolume). + */ +const CORE_AUDIO_CSHARP = ` +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace VddAudio { + [ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")] + public class MMDeviceEnumeratorCom { } + + [Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IMMDeviceEnumerator { + int EnumAudioEndpoints(int dataFlow, int stateMask, out IMMDeviceCollection devices); + int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice endpoint); + int GetDevice(string id, out IMMDevice device); + } + + [Guid("0BD7A1BE-7A1A-44DB-8397-CC5392387B5E"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IMMDeviceCollection { + int GetCount(out int count); + int Item(int index, out IMMDevice device); + } + + [Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IMMDevice { + int Activate(ref Guid iid, int clsCtx, IntPtr activationParams, [MarshalAs(UnmanagedType.IUnknown)] out object iface); + int OpenPropertyStore(int access, out IPropertyStore properties); + int GetId([MarshalAs(UnmanagedType.LPWStr)] out string id); + int GetState(out int state); + } + + [Guid("1BE09788-6894-4089-8586-9A2A6C265AC5"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IMMEndpoint { + int GetDataFlow(out int dataFlow); + } + + [StructLayout(LayoutKind.Sequential)] + public struct PropertyKey { public Guid fmtid; public int pid; } + + [StructLayout(LayoutKind.Sequential)] + public struct PropVariant { + public ushort vt; + public ushort r1; public ushort r2; public ushort r3; + public IntPtr p; + public int p2; + } + + [Guid("886d8eeb-8cf2-4446-8d02-cdba1dbdcf99"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IPropertyStore { + int GetCount(out int count); + int GetAt(int index, out PropertyKey key); + int GetValue(ref PropertyKey key, out PropVariant value); + int SetValue(ref PropertyKey key, ref PropVariant value); + int Commit(); + } + + [Guid("5CDF2C82-841E-4546-9722-0CF74078229A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IAudioEndpointVolume { + int RegisterControlChangeNotify(IntPtr notify); + int UnregisterControlChangeNotify(IntPtr notify); + int GetChannelCount(out int count); + int SetMasterVolumeLevel(float levelDb, ref Guid ctx); + int SetMasterVolumeLevelScalar(float level, ref Guid ctx); + int GetMasterVolumeLevel(out float levelDb); + int GetMasterVolumeLevelScalar(out float level); + int SetChannelVolumeLevel(int ch, float levelDb, ref Guid ctx); + int SetChannelVolumeLevelScalar(int ch, float level, ref Guid ctx); + int GetChannelVolumeLevel(int ch, out float levelDb); + int GetChannelVolumeLevelScalar(int ch, out float level); + int SetMute(bool mute, ref Guid ctx); + int GetMute(out bool mute); + } + + [ComImport, Guid("870af99c-171d-4f9e-af0d-e63df40c2bc9")] + public class PolicyConfigClientCom { } + + [Guid("f8679f50-850a-41cf-9c72-430f290290c8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IPolicyConfig { + int GetMixFormat(string id, IntPtr fmt); + int GetDeviceFormat(string id, bool isDefault, IntPtr fmt); + int ResetDeviceFormat(string id); + int SetDeviceFormat(string id, IntPtr a, IntPtr b); + int GetProcessingPeriod(string id, bool isDefault, IntPtr a, IntPtr b); + int SetProcessingPeriod(string id, IntPtr a); + int GetShareMode(string id, IntPtr mode); + int SetShareMode(string id, IntPtr mode); + int GetPropertyValue(string id, bool fxStore, IntPtr key, IntPtr pv); + int SetPropertyValue(string id, bool fxStore, IntPtr key, IntPtr pv); + int SetDefaultEndpoint(string id, int role); + int SetEndpointVisibility(string id, bool visible); + } + + public static class AudioCtl { + static Guid IID_IAudioEndpointVolume = new Guid("5CDF2C82-841E-4546-9722-0CF74078229A"); + + static IMMDeviceEnumerator Enumerator() { + return (IMMDeviceEnumerator)(object)(new MMDeviceEnumeratorCom()); + } + + static string DefaultId(IMMDeviceEnumerator en, int flow, int role) { + IMMDevice dev; + if (en.GetDefaultAudioEndpoint(flow, role, out dev) != 0) return ""; + string id; + dev.GetId(out id); + return id; + } + + static string FriendlyName(IMMDevice dev) { + IPropertyStore store; + if (dev.OpenPropertyStore(0, out store) != 0) return ""; + PropertyKey key = new PropertyKey(); + key.fmtid = new Guid("a45c254e-df1c-4efd-8020-67d146a850e0"); + key.pid = 14; + PropVariant pv; + if (store.GetValue(ref key, out pv) != 0) return ""; + if (pv.vt != 31 || pv.p == IntPtr.Zero) return ""; + return Marshal.PtrToStringUni(pv.p); + } + + static IAudioEndpointVolume Volume(IMMDevice dev) { + object o; + if (dev.Activate(ref IID_IAudioEndpointVolume, 23, IntPtr.Zero, out o) != 0) return null; + return (IAudioEndpointVolume)o; + } + + // Tab-separated: id, flow, isDefault, isDefaultComm, volume, muted, name + public static string ListTsv() { + IMMDeviceEnumerator en = Enumerator(); + string defRender = DefaultId(en, 0, 1); + string defRenderComm = DefaultId(en, 0, 2); + string defCapture = DefaultId(en, 1, 1); + string defCaptureComm = DefaultId(en, 1, 2); + + StringBuilder sb = new StringBuilder(); + IMMDeviceCollection col; + // eAll = 2, DEVICE_STATE_ACTIVE = 1 + if (en.EnumAudioEndpoints(2, 1, out col) != 0) return ""; + int count; + col.GetCount(out count); + for (int i = 0; i < count; i++) { + IMMDevice dev; + if (col.Item(i, out dev) != 0) continue; + string id; + dev.GetId(out id); + int flow = 0; + ((IMMEndpoint)dev).GetDataFlow(out flow); + string name = FriendlyName(dev); + float vol = 0; bool mute = false; + IAudioEndpointVolume v = Volume(dev); + if (v != null) { + v.GetMasterVolumeLevelScalar(out vol); + v.GetMute(out mute); + } + bool isDef = (flow == 0) ? (id == defRender) : (id == defCapture); + bool isDefComm = (flow == 0) ? (id == defRenderComm) : (id == defCaptureComm); + sb.Append(id).Append('\\t') + .Append(flow == 0 ? "render" : "capture").Append('\\t') + .Append(isDef ? "1" : "0").Append('\\t') + .Append(isDefComm ? "1" : "0").Append('\\t') + .Append(vol.ToString(System.Globalization.CultureInfo.InvariantCulture)).Append('\\t') + .Append(mute ? "1" : "0").Append('\\t') + .Append(name == null ? "" : name.Replace('\\t', ' ')) + .Append('\\n'); + } + return sb.ToString(); + } + + public static int SetDefault(string id) { + IPolicyConfig pc = (IPolicyConfig)(object)(new PolicyConfigClientCom()); + int rc = 0; + // eConsole=0, eMultimedia=1, eCommunications=2 + for (int role = 0; role <= 2; role++) { + int r = pc.SetDefaultEndpoint(id, role); + if (r != 0) rc = r; + } + return rc; + } + + public static int SetVolume(string id, float level) { + IMMDeviceEnumerator en = Enumerator(); + IMMDevice dev; + int r = en.GetDevice(id, out dev); + if (r != 0) return r; + IAudioEndpointVolume v = Volume(dev); + if (v == null) return -1; + Guid ctx = Guid.Empty; + return v.SetMasterVolumeLevelScalar(level, ref ctx); + } + + public static int SetMute(string id, bool mute) { + IMMDeviceEnumerator en = Enumerator(); + IMMDevice dev; + int r = en.GetDevice(id, out dev); + if (r != 0) return r; + IAudioEndpointVolume v = Volume(dev); + if (v == null) return -1; + Guid ctx = Guid.Empty; + return v.SetMute(mute, ref ctx); + } + } +} +`.trim() + +/** + * Windows audio endpoint control. Each call runs a short PowerShell process + * compiling the Core Audio interop above - no elevation required (volume, + * mute and default-device changes are per-user operations). + */ +export class AudioService { + private interopPath: string | null = null + + /** Writes the interop to a stable temp file once so scripts can dot-source it. */ + private async ensureInterop(): Promise { + if (this.interopPath) return this.interopPath + const path = join(app.getPath('userData'), 'core-audio-interop.cs') + await fs.writeFile(path, CORE_AUDIO_CSHARP, 'utf8') + this.interopPath = path + return path + } + + private async run(psBody: string): Promise { + const interop = await this.ensureInterop() + const script = [ + `$ErrorActionPreference = 'Stop'`, + `Add-Type -TypeDefinition (Get-Content -Raw -LiteralPath '${interop}')`, + psBody + ].join('\r\n') + const scriptPath = join(app.getPath('temp'), `vdd-audio-${Date.now()}-${Math.random().toString(36).slice(2)}.ps1`) + await fs.writeFile(scriptPath, script, 'utf8') + try { + const { stdout } = await execFileAsync( + 'powershell.exe', + ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', scriptPath], + { windowsHide: true, timeout: 30_000, maxBuffer: 4 * 1024 * 1024 } + ) + return stdout + } finally { + void fs.rm(scriptPath, { force: true }).catch(() => undefined) + } + } + + async listEndpoints(): Promise { + const stdout = await this.run(`[VddAudio.AudioCtl]::ListTsv() | Write-Output`) + const endpoints: AudioEndpoint[] = [] + for (const line of stdout.split('\n')) { + const parts = line.replace(/\r$/, '').split('\t') + if (parts.length < 7 || !ENDPOINT_ID_PATTERN.test(parts[0])) continue + const name = parts.slice(6).join(' ') + endpoints.push({ + id: parts[0], + flow: parts[1] === 'capture' ? 'capture' : 'render', + isDefault: parts[2] === '1', + isDefaultComm: parts[3] === '1', + volume: Math.min(1, Math.max(0, Number(parts[4]) || 0)), + muted: parts[5] === '1', + isVirtual: /virtual audio/i.test(name), + name + }) + } + endpoints.sort((a, b) => (a.flow === b.flow ? a.name.localeCompare(b.name) : a.flow === 'render' ? -1 : 1)) + return endpoints + } + + async setDefaultEndpoint(id: string): Promise { + this.assertId(id) + const out = await this.run(`$rc = [VddAudio.AudioCtl]::SetDefault('${id}'); Write-Output "RC=$rc"`) + this.assertRc(out, 'set default device') + } + + async setVolume(id: string, volume: number): Promise { + this.assertId(id) + const level = Math.min(1, Math.max(0, volume)) + const out = await this.run(`$rc = [VddAudio.AudioCtl]::SetVolume('${id}', ${level.toFixed(4)}); Write-Output "RC=$rc"`) + this.assertRc(out, 'set volume') + } + + async setMute(id: string, muted: boolean): Promise { + this.assertId(id) + const out = await this.run(`$rc = [VddAudio.AudioCtl]::SetMute('${id}', $${muted ? 'true' : 'false'}); Write-Output "RC=$rc"`) + this.assertRc(out, 'set mute') + } + + private assertId(id: string): void { + if (!ENDPOINT_ID_PATTERN.test(id)) throw new Error('Invalid audio endpoint id') + } + + private assertRc(stdout: string, operation: string): void { + const match = /RC=(-?\d+)/.exec(stdout) + if (!match || Number(match[1]) !== 0) { + throw new Error(`Failed to ${operation} (HRESULT ${match ? match[1] : 'unknown'})`) + } + } +} diff --git a/VirtualDriverControl/src/main/services/display-service.ts b/VirtualDriverControl/src/main/services/display-service.ts new file mode 100644 index 00000000..faff2abe --- /dev/null +++ b/VirtualDriverControl/src/main/services/display-service.ts @@ -0,0 +1,116 @@ +import { execFile } from 'child_process' +import { screen } from 'electron' +import { promisify } from 'util' +import type { DisplayLayoutInfo } from '@shared/types' + +const execFileAsync = promisify(execFile) + +const VIRTUAL_CACHE_TTL_MS = 30_000 + +/** + * Enumerates every attached display (physical and virtual) with real bounds, + * scale and placement from the Electron `screen` API, and flags monitors that + * hang off the MttVDD virtual adapter via PnP parent lookup. + */ +export class DisplayService { + private virtualNames: Set = new Set() + private virtualCheckedAt = 0 + private refreshing: Promise | null = null + private notify: (() => void) | null = null + + async layout(): Promise { + // Never block on the PnP lookup - heuristics cover the first paint and a + // push event refreshes the flags once the lookup lands. + void this.refreshVirtualNames() + const primaryId = screen.getPrimaryDisplay().id + return screen.getAllDisplays().map((d) => ({ + id: d.id, + label: d.label || 'Display', + bounds: { x: d.bounds.x, y: d.bounds.y, width: d.bounds.width, height: d.bounds.height }, + workArea: { x: d.workArea.x, y: d.workArea.y, width: d.workArea.width, height: d.workArea.height }, + scaleFactor: d.scaleFactor, + rotation: d.rotation, + frequency: Math.round(d.displayFrequency || 0), + internal: d.internal, + primary: d.id === primaryId, + isVirtual: this.isVirtualLabel(d.label), + colorDepth: d.colorDepth + })) + } + + /** Subscribes to display topology changes; returns an unsubscribe function. */ + watch(onChange: () => void): () => void { + this.notify = onChange + const handler = (): void => { + // Topology changed - virtual adapter may have gained/lost monitors. + this.virtualCheckedAt = 0 + onChange() + } + screen.on('display-added', handler) + screen.on('display-removed', handler) + screen.on('display-metrics-changed', handler) + return () => { + this.notify = null + screen.removeListener('display-added', handler) + screen.removeListener('display-removed', handler) + screen.removeListener('display-metrics-changed', handler) + } + } + + private isVirtualLabel(label: string): boolean { + if (!label) return false + const norm = label.trim().toLowerCase() + for (const name of this.virtualNames) { + if (name === norm || name.includes(norm) || norm.includes(name)) return true + } + // Fallback heuristics for when the PnP lookup has not resolved names yet. + return /vdd|virtual display/i.test(label) + } + + private async refreshVirtualNames(): Promise { + if (Date.now() - this.virtualCheckedAt < VIRTUAL_CACHE_TTL_MS) return + if (this.refreshing) return this.refreshing + this.refreshing = (async () => { + try { + // Adapter nodes are matched by hardware ID - instance IDs depend on how + // the node was created (ROOT\MTTVDD\... vs nefcon's ROOT\DISPLAY\...). + const script = [ + `$vddIds = @(Get-PnpDevice -ErrorAction SilentlyContinue | Where-Object { $_.HardwareID -match '^Root\\\\MttVDD$' } | ForEach-Object { $_.InstanceId })`, + `$mons = Get-PnpDevice -Class Monitor -PresentOnly -ErrorAction SilentlyContinue`, + `$out = foreach ($m in $mons) {`, + ` $parent = (Get-PnpDeviceProperty -InstanceId $m.InstanceId -KeyName 'DEVPKEY_Device_Parent' -ErrorAction SilentlyContinue).Data`, + ` if ($parent -and ($vddIds -contains $parent)) { $m.FriendlyName }`, + `}`, + `@($out) | ConvertTo-Json -Compress` + ].join('; ') + const { stdout } = await execFileAsync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], { + windowsHide: true, + timeout: 20_000 + }) + const trimmed = stdout.trim() + const parsed = trimmed ? (JSON.parse(trimmed) as string | string[] | null) : null + const names = parsed === null ? [] : Array.isArray(parsed) ? parsed : [parsed] + const next = new Set() + for (const raw of names) { + if (typeof raw !== 'string') continue + const full = raw.trim().toLowerCase() + if (full) next.add(full) + // PnP reports "Generic Monitor (Odyssey G95C)" while Electron labels + // are just "Odyssey G95C" - index the parenthesized name too. + const inner = /\(([^)]+)\)\s*$/.exec(raw)?.[1]?.trim().toLowerCase() + if (inner) next.add(inner) + } + const changed = next.size !== this.virtualNames.size || [...next].some((n) => !this.virtualNames.has(n)) + this.virtualNames = next + this.virtualCheckedAt = Date.now() + if (changed) this.notify?.() + } catch { + // keep the previous set; heuristics still apply + this.virtualCheckedAt = Date.now() + } finally { + this.refreshing = null + } + })() + return this.refreshing + } +} diff --git a/VirtualDriverControl/src/main/services/driver-service.ts b/VirtualDriverControl/src/main/services/driver-service.ts new file mode 100644 index 00000000..b36f0a1e --- /dev/null +++ b/VirtualDriverControl/src/main/services/driver-service.ts @@ -0,0 +1,254 @@ +import { execFile } from 'child_process' +import { existsSync, statSync } from 'fs' +import os from 'os' +import { promisify } from 'util' +import { app } from 'electron' +import type { DriverStatus, GpuInfo, SystemInfo } from '@shared/types' +import type { PipeClient } from './pipe-client' + +const execFileAsync = promisify(execFile) + +const DRIVER_DLL = 'C:\\Windows\\System32\\drivers\\UMDF\\MttVDD.dll' +const DEVICE_CACHE_TTL_MS = 30_000 + +interface DeviceInfo { + present: boolean + name?: string + pnpStatus?: string +} + +async function powershell(script: string, timeoutMs = 10_000): Promise { + const { stdout } = await execFileAsync( + 'powershell.exe', + ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', script], + { timeout: timeoutMs, windowsHide: true, maxBuffer: 4 * 1024 * 1024 } + ) + return stdout.trim() +} + +export class DriverService { + private deviceCache: { value: DeviceInfo; at: number } | null = null + private adminCache: boolean | null = null + + constructor( + private readonly pipe: PipeClient, + private readonly paths: { getBaseDir: () => string } + ) {} + + async status(force = false): Promise { + const pipeConnected = await this.pipe.ping() + const device = await this.queryDevice(force) + const dllPresent = existsSync(DRIVER_DLL) + let dllDate: string | undefined + if (dllPresent) { + try { + dllDate = statSync(DRIVER_DLL).mtime.toISOString().slice(0, 10) + } catch { + // ignore + } + } + + let level: DriverStatus['level'] = 'unknown' + if (pipeConnected) level = 'online' + else if (device.present || dllPresent) level = 'installed-offline' + else level = 'not-installed' + + return { + level, + pipeConnected, + devicePresent: device.present, + deviceName: device.name, + devicePnpStatus: device.pnpStatus, + dllPresent, + dllDate, + checkedAt: Date.now() + } + } + + /** + * GPU inventory. Prefers the driver's own enumeration (GETALLGPUS + + * GETASSIGNEDGPU); falls back to WMI when the pipe is offline. + */ + async gpus(): Promise { + const fromPipe = await this.gpusViaPipe() + if (fromPipe.length > 0) return fromPipe + return this.gpusViaWmi() + } + + async assignedGpu(): Promise { + const result = await this.pipe.send('GETASSIGNEDGPU') + if (!result.ok) return null + return extractGpuNames(result.lines)[0] ?? null + } + + /** + * Detected IddCx framework version, e.g. "1.10". + * + * The driver's IDDCXVERSION command only echoes "IDDCX Version: 0x…" over + * the pipe when file logging AND SendLogsThroughPipe are both enabled, so + * the pipe is best-effort. The reliable fallback maps the IddCx.dll (or OS) + * build number to the published IddCx version table. + */ + async iddcxVersion(): Promise { + const result = await this.pipe.send('IDDCXVERSION') + if (result.ok) { + const hex = result.response.match(/IDDCX[^\n]*?(0x[0-9a-fA-F]{3,8})/i)?.[1] + const decoded = hex ? decodeIddCxVersion(Number.parseInt(hex, 16)) : null + if (decoded) return decoded + } + const build = (await this.iddcxDllBuild()) ?? osBuildNumber() + return build !== null ? iddcxVersionForBuild(build) : null + } + + /** Build number of the IddCx framework binary that drivers actually load. */ + private async iddcxDllBuild(): Promise { + try { + const out = await powershell(`(Get-Item 'C:\\Windows\\System32\\drivers\\UMDF\\IddCx.dll').VersionInfo.FileBuildPart`) + const build = Number.parseInt(out, 10) + return Number.isFinite(build) ? build : null + } catch { + return null + } + } + + async isAdmin(): Promise { + if (this.adminCache !== null) return this.adminCache + try { + const out = await powershell( + `([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)` + ) + this.adminCache = out.toLowerCase().includes('true') + } catch { + this.adminCache = false + } + return this.adminCache + } + + async systemInfo(): Promise { + const isAdmin = await this.isAdmin() + return { + windowsVersion: os.version(), + windowsBuild: os.release(), + arch: os.arch(), + isAdmin, + appVersion: app.getVersion(), + electronVersion: process.versions.electron, + settingsPath: `${this.paths.getBaseDir()}\\vdd_settings.xml`, + logsDir: `${this.paths.getBaseDir()}\\Logs` + } + } + + private async queryDevice(force: boolean): Promise { + if (!force && this.deviceCache && Date.now() - this.deviceCache.at < DEVICE_CACHE_TTL_MS) { + return this.deviceCache.value + } + let value: DeviceInfo = { present: false } + try { + const out = await powershell( + `Get-CimInstance Win32_PnPEntity | Where-Object { ($_.DeviceID -like '*MttVDD*') -or ($_.Name -like '*Virtual Display Driver*') } | Select-Object Name, Status | ConvertTo-Json -Compress` + ) + if (out) { + const parsed: unknown = JSON.parse(out) + const first = Array.isArray(parsed) ? parsed[0] : parsed + if (first && typeof first === 'object') { + const rec = first as { Name?: string; Status?: string } + value = { present: true, name: rec.Name ?? 'Virtual Display Driver', pnpStatus: rec.Status } + } + } + } catch { + // WMI unavailable or no match - treat as not present. + } + this.deviceCache = { value, at: Date.now() } + return value + } + + private async gpusViaPipe(): Promise { + const all = await this.pipe.send('GETALLGPUS') + if (!all.ok) return [] + const names = extractGpuNames(all.lines) + if (names.length === 0) return [] + const assigned = await this.assignedGpu() + return names.map((name) => ({ + name, + source: 'pipe' as const, + assigned: assigned !== null && name.toLowerCase() === assigned.toLowerCase() + })) + } + + private async gpusViaWmi(): Promise { + try { + const out = await powershell( + `Get-CimInstance Win32_VideoController | Select-Object Name, DriverVersion, AdapterRAM | ConvertTo-Json -Compress` + ) + if (!out) return [] + const parsed: unknown = JSON.parse(out) + const list = Array.isArray(parsed) ? parsed : [parsed] + return list + .filter((g): g is { Name?: string; DriverVersion?: string; AdapterRAM?: number } => !!g && typeof g === 'object') + .filter((g) => typeof g.Name === 'string' && g.Name.length > 0) + .map((g) => ({ + name: g.Name as string, + source: 'wmi' as const, + assigned: false, + driverVersion: g.DriverVersion, + vramMB: typeof g.AdapterRAM === 'number' && g.AdapterRAM > 0 ? Math.round(g.AdapterRAM / 1024 / 1024) : undefined + })) + } catch { + return [] + } + } +} + +/** + * IddCxGetVersion values are nibble-encoded per Microsoft's IddCx versions + * table: 0x1500 → 1.5, 0x1A00/0x1A80 → 1.10, 0x1B00 → 1.11. + */ +function decodeIddCxVersion(value: number): string | null { + if (!Number.isFinite(value) || value < 0x1000) return null + return `${value >> 12}.${(value >> 8) & 0xf}` +} + +/** Windows build → shipped IddCx version (learn.microsoft.com, iddcx-versions). */ +const BUILD_TO_IDDCX: Array<[minBuild: number, version: string]> = [ + [26100, '1.10'], + [22631, '1.10'], + [22621, '1.9'], + [22000, '1.8'], + [19041, '1.5'], + [18362, '1.4'] +] + +function iddcxVersionForBuild(build: number): string | null { + for (const [minBuild, version] of BUILD_TO_IDDCX) { + if (build >= minBuild) return version + } + return null +} + +/** Build number from os.release(), e.g. "10.0.26200" → 26200. */ +function osBuildNumber(): number | null { + const match = os.release().match(/^\d+\.\d+\.(\d+)/) + if (!match) return null + const build = Number.parseInt(match[1], 10) + return Number.isFinite(build) ? build : null +} + +/** + * The pipe responds to GPU queries with free-form log lines. Pull out + * plausible GPU names: lines after "GPU:" markers or lines that look like + * adapter names. + */ +function extractGpuNames(lines: string[]): string[] { + const names: string[] = [] + for (const line of lines) { + const marker = line.match(/GPU(?:\s*\d*)?\s*[:=]\s*(.+)$/i) + if (marker) { + const name = marker[1].trim() + if (name && !/^(none|null|unknown)$/i.test(name)) names.push(name) + continue + } + const adapter = line.match(/\b((?:NVIDIA|AMD|Intel|Microsoft|Qualcomm|Radeon|GeForce|Arc)\b[^|;]{2,70})/i) + if (adapter) names.push(adapter[1].trim()) + } + return Array.from(new Set(names)) +} diff --git a/VirtualDriverControl/src/main/services/installer-service.ts b/VirtualDriverControl/src/main/services/installer-service.ts new file mode 100644 index 00000000..e3d3747e --- /dev/null +++ b/VirtualDriverControl/src/main/services/installer-service.ts @@ -0,0 +1,716 @@ +import { execFile } from 'child_process' +import { createHash } from 'crypto' +import { app } from 'electron' +import { createWriteStream, promises as fs } from 'fs' +import os from 'os' +import { join } from 'path' +import { promisify } from 'util' +import type { InstallProgress, LifecycleResult, ManagedDeviceState, ManagedDriverId, ReleaseInfo } from '@shared/types' + +const execFileAsync = promisify(execFile) + +const ALLOWED_DOWNLOAD_HOSTS = new Set(['github.com', 'objects.githubusercontent.com', 'release-assets.githubusercontent.com']) + +/** + * Pinned nefcon release (Nefarius device console). This is the tool the + * official Virtual Display Driver setup uses for device-node creation and + * driver installation. Downloaded on demand and verified against the SHA-256 + * digest GitHub publishes for the release asset. + */ +const NEFCON = { + tag: 'v1.17.40', + url: 'https://github.com/nefarius/nefcon/releases/download/v1.17.40/nefcon_v1.17.40.zip', + sha256: '812bae7ed7dfb7d6d2284bc7de2f8ccebc92ed2a0b1ae893c53b337096e50c1a' +} + +interface DriverSpec { + title: string + repo: string + pickAsset: (names: string[]) => string | undefined + infName: string + /** File whose Authenticode signature gets trusted before install. */ + signedBinary: string + hardwareId: string + /** + * PowerShell regex matched against Get-PnpDevice .HardwareID. Instance IDs + * vary by how the node was created (nefcon yields ROOT\DISPLAY\000x), so + * the hardware ID is the only reliable way to find our devices. + */ + hardwareIdPattern: string + /** Setup class name + GUID (brace-free, as nefcon expects). */ + className: string + classGuid: string + deviceDescription: string + /** Copy the package contents to the configured base dir (display driver keeps its settings there). */ + copyToBaseDir: boolean + /** Files never overwritten during the copy. */ + preserveFiles: string[] + maxInstances: number +} + +const DRIVERS: Record = { + display: { + title: 'Virtual Display Driver', + repo: 'VirtualDrivers/Virtual-Display-Driver', + pickAsset: (names) => { + const wantArm = os.arch() === 'arm64' + return names.find((n) => + wantArm ? /VirtualDisplayDriver-ARM64\.Driver\.Only\.zip/i.test(n) : /VirtualDisplayDriver-x(86|64)\.Driver\.Only\.zip/i.test(n) + ) + }, + infName: 'MttVDD.inf', + signedBinary: 'MttVDD.dll', + hardwareId: 'Root\\MttVDD', + hardwareIdPattern: '^Root\\\\MttVDD$', + className: 'Display', + classGuid: '4D36E968-E325-11CE-BFC1-08002BE10318', + deviceDescription: 'Virtual Display Driver', + copyToBaseDir: true, + preserveFiles: ['vdd_settings.xml'], + maxInstances: 1 + }, + audio: { + title: 'Virtual Audio Driver', + repo: 'VirtualDrivers/Virtual-Audio-Driver', + pickAsset: (names) => names.find((n) => /\.zip$/i.test(n)), + infName: 'VirtualAudioDriver.inf', + signedBinary: 'VirtualAudioDriver.sys', + hardwareId: 'Root\\VirtualAudioDriver', + hardwareIdPattern: '^Root\\\\VirtualAudioDriver$', + className: 'MEDIA', + classGuid: '4D36E96C-E325-11CE-BFC1-08002BE10318', + deviceDescription: 'Virtual Audio Driver', + copyToBaseDir: false, + preserveFiles: [], + maxInstances: 4 + } +} + +interface GithubAsset { + name: string + size: number + browser_download_url: string + digest?: string +} + +interface GithubRelease { + tag_name: string + name: string + published_at: string + body: string + html_url: string + assets: GithubAsset[] +} + +/** + * Downloads the latest signed driver packages from the official VirtualDrivers + * releases and manages the device lifecycle (install / uninstall / restart / + * instance count) through elevated PowerShell. Device-node creation and driver + * installation go through nefcon - the same tool the official VDD setup uses - + * which is fetched on demand from its pinned GitHub release and SHA-256 + * verified. pnputil remains in use for restarts and driver-store cleanup. + */ +export class InstallerService { + private busy = false + + constructor( + private readonly getBaseDir: () => string, + private readonly isAdmin: () => Promise, + private readonly emitProgress: (progress: InstallProgress) => void + ) {} + + // ------------------------------------------------------------------------- + // Release discovery / state + // ------------------------------------------------------------------------- + + async latestRelease(driver: ManagedDriverId): Promise { + const spec = DRIVERS[driver] + const response = await fetch(`https://api.github.com/repos/${spec.repo}/releases/latest`, { + headers: { 'User-Agent': 'Virtual-Driver-Control', Accept: 'application/vnd.github+json' } + }) + if (!response.ok) throw new Error(`GitHub API responded ${response.status}`) + const release = (await response.json()) as GithubRelease + + const assetName = spec.pickAsset(release.assets.map((a) => a.name)) + const asset = release.assets.find((a) => a.name === assetName) + + return { + tag: release.tag_name, + name: release.name, + publishedAt: release.published_at, + notes: release.body ?? '', + htmlUrl: release.html_url, + asset: asset + ? { + name: asset.name, + sizeBytes: asset.size, + downloadUrl: asset.browser_download_url, + sha256: asset.digest?.startsWith('sha256:') ? asset.digest.slice(7) : undefined + } + : null + } + } + + async installedReleaseTag(driver: ManagedDriverId): Promise { + const read = async (path: string): Promise => { + try { + const marker = JSON.parse(await fs.readFile(path, 'utf8')) as { tag?: string } + return marker.tag ?? null + } catch { + return null + } + } + const tag = await read(this.markerPath(driver)) + if (tag) return tag + // Legacy location used by the first installer iteration (display only). + if (driver === 'display') return read(join(this.getBaseDir(), 'installed_release.json')) + return null + } + + /** Non-elevated device presence/status query. */ + async deviceState(driver: ManagedDriverId): Promise { + const spec = DRIVERS[driver] + try { + const { stdout } = await execFileAsync( + 'powershell.exe', + [ + '-NoProfile', + '-NonInteractive', + '-Command', + `Get-PnpDevice -ErrorAction SilentlyContinue | Where-Object { $_.HardwareID -match '${spec.hardwareIdPattern}' } | ForEach-Object { $_.Status } | ConvertTo-Json -Compress` + ], + { windowsHide: true, timeout: 20_000 } + ) + const trimmed = stdout.trim() + if (!trimmed) return { count: 0, statuses: [] } + const parsed = JSON.parse(trimmed) as string | string[] + const statuses = Array.isArray(parsed) ? parsed : [parsed] + return { count: statuses.length, statuses } + } catch { + return { count: 0, statuses: [] } + } + } + + /** + * Whether the boot configuration has test signing enabled. The Virtual + * Audio Driver is currently only test-signed, so its device will not start + * without it. Returns null when the state cannot be determined (bcdedit + * needs administrator rights). + */ + async testSigningEnabled(): Promise { + try { + const { stdout } = await execFileAsync('bcdedit', ['/enum', '{current}'], { windowsHide: true, timeout: 15_000 }) + return /testsigning\s+Yes/i.test(stdout) + } catch { + return null + } + } + + /** Toggles Windows test signing via bcdedit (takes effect after a restart). */ + async setTestSigning(enabled: boolean): Promise { + return this.exclusive(async () => { + this.emitProgress({ + phase: 'install', + percent: -1, + message: `${enabled ? 'Enabling' : 'Disabling'} Windows test signing (this may prompt for elevation)…` + }) + return this.runElevated(` +Write-Output "== Windows test signing: ${enabled ? 'enable' : 'disable'} ==" +$out = bcdedit /set "{current}" testsigning ${enabled ? 'on' : 'off'} 2>&1 | Out-String +Write-Output $out.Trim() +if ($LASTEXITCODE -ne 0) { + if ($out -match 'Secure Boot') { + Write-Output "RESULT: blocked by Secure Boot - disable Secure Boot in the UEFI firmware settings first" + } else { + Write-Output "RESULT: bcdedit failed with exit code $LASTEXITCODE" + } + $script:failed = $true +} else { + Write-Output "RESULT: test signing ${enabled ? 'enabled' : 'disabled'} - restart Windows for the change to take effect" +} +`) + }) + } + + // ------------------------------------------------------------------------- + // Lifecycle operations + // ------------------------------------------------------------------------- + + async downloadAndInstall(driver: ManagedDriverId, instances = 1): Promise { + const spec = DRIVERS[driver] + const target = clampInstances(instances, spec) + return this.exclusive(async () => { + const release = await this.latestRelease(driver) + if (!release.asset) return { ok: false, error: `Release ${release.tag} has no driver package for ${os.arch()}` } + + const url = new URL(release.asset.downloadUrl) + if (url.protocol !== 'https:' || !ALLOWED_DOWNLOAD_HOSTS.has(url.hostname)) { + return { ok: false, error: `Refusing download from unexpected host: ${url.hostname}` } + } + + // 1. Download with progress. + const workDir = join(app.getPath('temp'), `vdd-install-${driver}-${Date.now()}`) + await fs.mkdir(workDir, { recursive: true }) + const zipPath = join(workDir, release.asset.name) + await this.downloadFile(release.asset.downloadUrl, zipPath, release.asset.sizeBytes) + + // 2. Verify checksum against the digest GitHub publishes for the asset. + this.emitProgress({ phase: 'verify', percent: -1, message: 'Verifying package integrity…' }) + if (release.asset.sha256) { + const actual = createHash('sha256').update(await fs.readFile(zipPath)).digest('hex') + if (actual.toLowerCase() !== release.asset.sha256.toLowerCase()) { + return { ok: false, error: `Checksum mismatch - expected ${release.asset.sha256}, got ${actual}` } + } + } + + // 3. Extract. + this.emitProgress({ phase: 'extract', percent: -1, message: 'Extracting driver package…' }) + const extractDir = join(workDir, 'extracted') + await execFileAsync( + 'powershell.exe', + ['-NoProfile', '-NonInteractive', '-Command', `Expand-Archive -Force -LiteralPath '${zipPath}' -DestinationPath '${extractDir}'`], + { windowsHide: true, timeout: 60_000 } + ) + const infPath = await this.findFile(extractDir, new RegExp(`^${spec.infName.replace('.', '\\.')}$`, 'i')) + if (!infPath) return { ok: false, error: `Driver package did not contain ${spec.infName}` } + const packageDir = infPath.slice(0, infPath.lastIndexOf('\\')) + + // 4. Make sure the nefcon install tool is available. + const nefcon = await this.ensureNefcon() + + // 5. Elevated install. + this.emitProgress({ phase: 'install', percent: -1, message: `Installing ${spec.title} (this may prompt for elevation)…` }) + const result = await this.runElevated(this.buildInstallScript(driver, packageDir, target, nefcon)) + if (!result.ok) return result + + // 6. Record what we installed. + this.emitProgress({ phase: 'finalize', percent: -1, message: 'Finishing up…' }) + try { + await fs.writeFile( + this.markerPath(driver), + JSON.stringify({ tag: release.tag, asset: release.asset.name, installedAt: new Date().toISOString() }, null, 2), + 'utf8' + ) + } catch { + // marker is best-effort + } + await fs.rm(workDir, { recursive: true, force: true }).catch(() => undefined) + return result + }) + } + + async uninstall(driver: ManagedDriverId): Promise { + return this.exclusive(async () => { + const nefcon = await this.ensureNefcon() + this.emitProgress({ phase: 'install', percent: -1, message: `Removing ${DRIVERS[driver].title} (this may prompt for elevation)…` }) + const result = await this.runElevated(this.buildUninstallScript(driver, nefcon)) + if (result.ok) await fs.rm(this.markerPath(driver), { force: true }).catch(() => undefined) + return result + }) + } + + async restartDevice(driver: ManagedDriverId): Promise { + return this.exclusive(async () => { + this.emitProgress({ phase: 'install', percent: -1, message: 'Restarting device (this may prompt for elevation)…' }) + return this.runElevated(this.buildRestartScript(driver)) + }) + } + + /** Create or remove device nodes so exactly `count` instances exist (audio driver). */ + async setInstances(driver: ManagedDriverId, count: number): Promise { + const spec = DRIVERS[driver] + if (spec.maxInstances < 2) return { ok: false, error: `${spec.title} does not support multiple instances` } + const target = clampInstances(count, spec) + return this.exclusive(async () => { + const nefcon = await this.ensureNefcon() + this.emitProgress({ phase: 'install', percent: -1, message: `Setting ${spec.title} to ${target} device${target === 1 ? '' : 's'}…` }) + return this.runElevated(this.buildSetInstancesScript(driver, target, nefcon)) + }) + } + + // ------------------------------------------------------------------------- + // Internals + // ------------------------------------------------------------------------- + + private markerPath(driver: ManagedDriverId): string { + return join(app.getPath('userData'), `installed_release_${driver}.json`) + } + + /** + * Ensures the pinned nefcon release is cached locally and returns the path + * to the console binary for this CPU architecture. The console flavor + * (nefconc) is used so its output lands in the elevated transcript; the + * window stays hidden either way. + */ + private async ensureNefcon(): Promise { + const archDir = os.arch() === 'arm64' ? 'ARM64' : os.arch() === 'ia32' ? 'x86' : 'x64' + const toolDir = join(app.getPath('userData'), 'tools', `nefcon-${NEFCON.tag}`) + const exePath = join(toolDir, archDir, 'nefconc.exe') + if (await pathExists(exePath)) return exePath + + this.emitProgress({ phase: 'download', percent: -1, message: `Downloading nefcon ${NEFCON.tag} (device install tool)…` }) + const zipPath = join(app.getPath('temp'), `nefcon-${Date.now()}.zip`) + try { + await this.downloadFile(NEFCON.url, zipPath, 0) + + this.emitProgress({ phase: 'verify', percent: -1, message: 'Verifying nefcon integrity…' }) + const actual = createHash('sha256').update(await fs.readFile(zipPath)).digest('hex') + if (actual.toLowerCase() !== NEFCON.sha256) { + throw new Error(`nefcon download failed checksum verification (expected ${NEFCON.sha256}, got ${actual})`) + } + + await fs.mkdir(toolDir, { recursive: true }) + await execFileAsync( + 'powershell.exe', + ['-NoProfile', '-NonInteractive', '-Command', `Expand-Archive -Force -LiteralPath '${zipPath}' -DestinationPath '${toolDir}'`], + { windowsHide: true, timeout: 60_000 } + ) + if (!(await pathExists(exePath))) throw new Error('nefcon package did not contain the expected binary') + return exePath + } finally { + void fs.rm(zipPath, { force: true }).catch(() => undefined) + } + } + + private async exclusive(operation: () => Promise): Promise { + if (this.busy) return { ok: false, error: 'Another driver operation is already running' } + this.busy = true + try { + return await operation() + } catch (error) { + return { ok: false, error: error instanceof Error ? error.message : String(error) } + } finally { + this.busy = false + } + } + + private async downloadFile(url: string, destination: string, expectedSize: number): Promise { + const response = await fetch(url, { headers: { 'User-Agent': 'Virtual-Driver-Control' } }) + if (!response.ok || !response.body) throw new Error(`Download failed with HTTP ${response.status}`) + + const total = Number(response.headers.get('content-length')) || expectedSize || 0 + const out = createWriteStream(destination) + const reader = response.body.getReader() + let received = 0 + try { + for (;;) { + const { done, value } = await reader.read() + if (done) break + received += value.byteLength + if (!out.write(Buffer.from(value))) { + await new Promise((resolve) => out.once('drain', resolve)) + } + this.emitProgress({ + phase: 'download', + percent: total > 0 ? Math.round((received / total) * 100) : -1, + message: `Downloading ${(received / 1024).toFixed(0)} KB${total > 0 ? ` of ${(total / 1024).toFixed(0)} KB` : ''}…` + }) + } + } finally { + await new Promise((resolve) => out.end(resolve)) + } + } + + private async findFile(root: string, pattern: RegExp): Promise { + const entries = await fs.readdir(root, { withFileTypes: true }) + for (const entry of entries) { + const full = join(root, entry.name) + if (entry.isFile() && pattern.test(entry.name)) return full + if (entry.isDirectory()) { + const nested = await this.findFile(full, pattern) + if (nested) return nested + } + } + return null + } + + /** + * Runs a PowerShell script with admin rights. If the app is already + * elevated it runs inline; otherwise a single UAC prompt is triggered. + * Output is captured through a temp log file in both cases. + */ + private async runElevated(script: string): Promise { + const stamp = Date.now() + const scriptPath = join(app.getPath('temp'), `vdd-op-${stamp}.ps1`) + const logPath = join(app.getPath('temp'), `vdd-op-${stamp}.log`) + + const wrapped = [ + `$ErrorActionPreference = 'Continue'`, + `Start-Transcript -Path '${logPath}' -Force | Out-Null`, + `$script:failed = $false`, + script, + `Stop-Transcript | Out-Null`, + `if ($script:failed) { exit 1 } else { exit 0 }` + ].join('\r\n') + await fs.writeFile(scriptPath, wrapped, 'utf8') + + try { + const elevated = await this.isAdmin() + let exitCode: number + if (elevated) { + try { + await execFileAsync('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', scriptPath], { + windowsHide: true, + timeout: 300_000 + }) + exitCode = 0 + } catch (error) { + exitCode = (error as { code?: number }).code ?? 1 + } + } else { + // -Verb RunAs cannot capture output directly; the transcript log covers that. + const launcher = `$p = Start-Process powershell.exe -Verb RunAs -Wait -PassThru -WindowStyle Hidden -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-File','${scriptPath}'; exit $p.ExitCode` + try { + await execFileAsync('powershell.exe', ['-NoProfile', '-Command', launcher], { windowsHide: true, timeout: 300_000 }) + exitCode = 0 + } catch (error) { + const failure = error as { code?: number; message?: string; stderr?: string } + // UAC decline surfaces as a launcher error ("canceled by the user"). + if (/canceled|cancelled/i.test(`${failure.message ?? ''} ${failure.stderr ?? ''}`)) { + return { ok: false, error: 'Elevation was declined - the operation was cancelled.' } + } + exitCode = failure.code ?? 1 + } + } + + const log = await fs.readFile(logPath, 'utf8').catch(() => '') + const detail = summarizeTranscript(log) + if (exitCode === 0) return { ok: true, detail } + return { ok: false, error: `Operation failed (exit ${exitCode})`, detail } + } finally { + void fs.rm(scriptPath, { force: true }).catch(() => undefined) + void fs.rm(logPath, { force: true }).catch(() => undefined) + } + } + + // ------------------------------------------------------------------------- + // Elevated script builders + // ------------------------------------------------------------------------- + + private buildInstallScript(driver: ManagedDriverId, packageDir: string, instances: number, nefconPath: string): string { + const spec = DRIVERS[driver] + const copyBlock = spec.copyToBaseDir + ? ` +# --- Copy package to the driver folder (never overwrite user settings) --- +$base = '${this.getBaseDir()}' +New-Item -ItemType Directory -Force -Path $base | Out-Null +$preserve = @(${spec.preserveFiles.map((f) => `'${f}'`).join(',')}) +foreach ($f in Get-ChildItem -File $pkg) { + if (($preserve -contains $f.Name) -and (Test-Path (Join-Path $base $f.Name))) { + Write-Output ("Keeping existing " + $f.Name) + continue + } + Copy-Item -Force $f.FullName (Join-Path $base $f.Name) +} + +# --- Point the driver's settings lookup (VDDPATH) at this folder --- +New-Item -Path 'HKLM:\\SOFTWARE\\MikeTheTech\\VirtualDisplayDriver' -Force | Out-Null +Set-ItemProperty -Path 'HKLM:\\SOFTWARE\\MikeTheTech\\VirtualDisplayDriver' -Name 'VDDPATH' -Value $base -Force +Write-Output "VDDPATH registry value set to $base" +` + : '' + + // Single-instance drivers also clean up stray duplicate nodes from earlier + // failed installs; multi-instance drivers must keep their duplicates. + const installFlags = spec.maxInstances === 1 ? '--no-duplicates --remove-duplicates' : '--no-duplicates' + + return ` +$pkg = '${packageDir}' +$inf = Join-Path $pkg '${spec.infName}' +$bin = Join-Path $pkg '${spec.signedBinary}' +$nefcon = '${nefconPath}' +$target = ${instances} + +Write-Output "== ${spec.title} install ==" +${copyBlock} +# --- Trust the package signer so the driver installs silently --- +$sig = Get-AuthenticodeSignature $bin +if ($sig.SignerCertificate) { + Write-Output ("Trusting signer: " + $sig.SignerCertificate.Subject) + foreach ($storeName in @('TrustedPublisher','Root')) { + $store = New-Object System.Security.Cryptography.X509Certificates.X509Store($storeName, 'LocalMachine') + $store.Open('ReadWrite') + $store.Add($sig.SignerCertificate) + $store.Close() + } +} else { + Write-Output "WARNING: package is unsigned" +} + +# --- Create the device node and install the driver via nefcon --- +Write-Output "Installing driver via nefcon (devcon-compatible install)" +& $nefcon install "$inf" '${spec.hardwareId}' ${installFlags} 2>&1 | ForEach-Object { Write-Output $_ } +$rc = $LASTEXITCODE +if ($rc -eq 3010) { + Write-Output "NOTE: Windows reports a reboot is required to finish the install" +} elseif ($rc -ne 0) { + Write-Output "nefcon install failed with exit code $rc" + $script:failed = $true +} + +# --- Create any additional device nodes up to the target count --- +$existing = @(Get-PnpDevice -ErrorAction SilentlyContinue | Where-Object { $_.HardwareID -match '${spec.hardwareIdPattern}' }) +Write-Output ("Existing device nodes: " + $existing.Count + ", target: " + $target) +$toCreate = $target - $existing.Count +if (-not $script:failed -and $toCreate -gt 0) { + for ($i = 0; $i -lt $toCreate; $i++) { + & $nefcon --create-device-node --hardware-id '${spec.hardwareId}' --class-name '${spec.className}' --class-guid '${spec.classGuid}' 2>&1 | ForEach-Object { Write-Output $_ } + if ($LASTEXITCODE -ne 0) { + Write-Output "Device node creation failed with exit code $LASTEXITCODE" + $script:failed = $true + break + } + Write-Output ("Device node " + ($existing.Count + $i + 1) + " created") + } + # Bind the staged driver to the freshly created nodes. + & $nefcon --install-driver --inf-path "$inf" 2>&1 | ForEach-Object { Write-Output $_ } +} + +# --- Verify --- +Start-Sleep -Seconds 2 +$dev = @(Get-PnpDevice -ErrorAction SilentlyContinue | Where-Object { $_.HardwareID -match '${spec.hardwareIdPattern}' }) +if ($dev.Count -gt 0) { + Write-Output ("RESULT: " + $dev.Count + " device(s) present, status " + (($dev | ForEach-Object { $_.Status }) -join ', ')) +} else { + Write-Output "RESULT: device not found after install" + $script:failed = $true +} +` + } + + private buildUninstallScript(driver: ManagedDriverId, nefconPath: string): string { + const spec = DRIVERS[driver] + return ` +$nefcon = '${nefconPath}' +Write-Output "== ${spec.title} uninstall ==" + +# --- Remove the device node(s) and driver via nefcon --- +$devices = @(Get-PnpDevice -ErrorAction SilentlyContinue | Where-Object { $_.HardwareID -match '${spec.hardwareIdPattern}' }) +if ($devices.Count -gt 0) { + Write-Output ("Removing " + $devices.Count + " device node(s) via nefcon") + & $nefcon --remove-device-node --hardware-id '${spec.hardwareId}' --class-guid '${spec.classGuid}' 2>&1 | ForEach-Object { Write-Output $_ } + if ($LASTEXITCODE -ne 0 -and $LASTEXITCODE -ne 3010) { + Write-Output "nefcon remove-device-node failed with exit code $LASTEXITCODE" + $script:failed = $true + } +} else { + Write-Output "No ${spec.title} device present" +} + +# --- Sweep any leftover staged package(s) out of the driver store --- +$enum = pnputil /enum-drivers | Out-String +$blocks = $enum -split '(?=Published Name)' +foreach ($b in $blocks) { + if ($b -match '${spec.infName.replace('.', '\\.')}') { + if ($b -match 'Published Name\\s*:\\s*(oem\\d+\\.inf)') { + $oem = $Matches[1] + Write-Output "Deleting driver package $oem" + pnputil /delete-driver $oem /uninstall /force 2>&1 | ForEach-Object { Write-Output $_ } + } + } +} + +Write-Output "RESULT: uninstall complete (configuration files were kept)" +` + } + + private buildRestartScript(driver: ManagedDriverId): string { + const spec = DRIVERS[driver] + return ` +Write-Output "== ${spec.title} device restart ==" +$devices = Get-PnpDevice -ErrorAction SilentlyContinue | Where-Object { $_.HardwareID -match '${spec.hardwareIdPattern}' } +if (-not $devices) { + Write-Output "RESULT: no device found" + $script:failed = $true +} else { + foreach ($d in $devices) { + Write-Output ("Restarting " + $d.InstanceId) + pnputil /disable-device $d.InstanceId 2>&1 | ForEach-Object { Write-Output $_ } + Start-Sleep -Seconds 2 + pnputil /enable-device $d.InstanceId 2>&1 | ForEach-Object { Write-Output $_ } + } + Start-Sleep -Seconds 2 + $after = Get-PnpDevice -ErrorAction SilentlyContinue | Where-Object { $_.HardwareID -match '${spec.hardwareIdPattern}' -and $_.Status -eq 'OK' } + if ($after) { Write-Output "RESULT: device restarted, status OK" } + else { Write-Output "RESULT: device did not come back healthy"; $script:failed = $true } +} +` + } + + private buildSetInstancesScript(driver: ManagedDriverId, target: number, nefconPath: string): string { + const spec = DRIVERS[driver] + return ` +$nefcon = '${nefconPath}' +$target = ${target} +Write-Output "== ${spec.title}: set instance count to $target ==" + +# --- Locate the staged INF (needed to bind newly created devices) --- +$oem = $null +$enum = pnputil /enum-drivers | Out-String +$blocks = $enum -split '(?=Published Name)' +foreach ($b in $blocks) { + if ($b -match '${spec.infName.replace('.', '\\.')}' -and $b -match 'Published Name\\s*:\\s*(oem\\d+\\.inf)') { + $oem = $Matches[1] + } +} +if (-not $oem) { + Write-Output "RESULT: driver is not installed - install it first" + $script:failed = $true +} else { + $infPath = Join-Path $env:windir ('INF\\' + $oem) + Write-Output "Using staged driver $oem" + + $existing = @(Get-PnpDevice -ErrorAction SilentlyContinue | Where-Object { $_.HardwareID -match '${spec.hardwareIdPattern}' } | Sort-Object InstanceId) + Write-Output ("Existing device nodes: " + $existing.Count) + + if ($existing.Count -lt $target) { + for ($i = $existing.Count; $i -lt $target; $i++) { + & $nefcon --create-device-node --hardware-id '${spec.hardwareId}' --class-name '${spec.className}' --class-guid '${spec.classGuid}' 2>&1 | ForEach-Object { Write-Output $_ } + if ($LASTEXITCODE -ne 0) { + Write-Output "Device node creation failed with exit code $LASTEXITCODE" + $script:failed = $true + break + } + Write-Output ("Device node " + ($i + 1) + " created") + } + # Bind the staged driver to the freshly created nodes. + & $nefcon --install-driver --inf-path "$infPath" 2>&1 | ForEach-Object { Write-Output $_ } + } elseif ($existing.Count -gt $target) { + $toRemove = $existing | Select-Object -Last ($existing.Count - $target) + foreach ($d in $toRemove) { + Write-Output ("Removing device " + $d.InstanceId) + pnputil /remove-device $d.InstanceId 2>&1 | ForEach-Object { Write-Output $_ } + } + } else { + Write-Output "Already at target count" + } + + Start-Sleep -Seconds 2 + $after = @(Get-PnpDevice -ErrorAction SilentlyContinue | Where-Object { $_.HardwareID -match '${spec.hardwareIdPattern}' }) + Write-Output ("RESULT: " + $after.Count + " device(s) present, status " + (($after | ForEach-Object { $_.Status }) -join ', ')) + if ($after.Count -ne $target) { $script:failed = $true } +} +` + } +} + +function pathExists(path: string): Promise { + return fs.access(path).then( + () => true, + () => false + ) +} + +function clampInstances(value: number, spec: DriverSpec): number { + if (!Number.isFinite(value)) return 1 + return Math.min(Math.max(Math.round(value), 1), spec.maxInstances) +} + +/** Pull the useful tail out of a PowerShell transcript. */ +function summarizeTranscript(log: string): string { + const lines = log + .split(/\r?\n/) + .filter((l) => l.trim().length > 0) + .filter((l) => !/^\*{10,}|^Windows PowerShell transcript|^Start time|^End time|^Username|^RunAs User|^Configuration Name|^Machine|^Host Application|^Process ID|^PSVersion|^PSEdition|^PSCompatibleVersions|^BuildVersion|^CLRVersion|^WSManStackVersion|^PSRemotingProtocolVersion|^SerializationVersion|^Transcript started|^\s*$/.test(l)) + return lines.slice(-25).join('\n') +} diff --git a/VirtualDriverControl/src/main/services/log-service.ts b/VirtualDriverControl/src/main/services/log-service.ts new file mode 100644 index 00000000..39cbc60d --- /dev/null +++ b/VirtualDriverControl/src/main/services/log-service.ts @@ -0,0 +1,142 @@ +import { EventEmitter } from 'events' +import { promises as fs } from 'fs' +import { existsSync } from 'fs' +import { join } from 'path' +import type { LogEvent, LogSeverity, PipeResult } from '@shared/types' +import type { PipeClient } from './pipe-client' + +const POLL_INTERVAL_MS = 1_500 +const RING_BUFFER_SIZE = 3_000 +const INITIAL_TAIL_BYTES = 64 * 1024 + +/** + * Streams driver activity into a single unified feed: + * - tails the daily file log at \Logs\log_YYYY-MM-DD.txt + * - captures every pipe command's streamed response lines + */ +export class LogService extends EventEmitter { + private buffer: LogEvent[] = [] + private nextId = 1 + private timer: NodeJS.Timeout | null = null + private currentFile: string | null = null + private offset = 0 + private pendingPartial = '' + + constructor( + private readonly pipe: PipeClient, + private readonly getBaseDir: () => string + ) { + super() + this.pipe.on('result', (result: PipeResult) => this.capturePipeResult(result)) + } + + start(): void { + if (this.timer) return + this.timer = setInterval(() => void this.pollFile(), POLL_INTERVAL_MS) + void this.pollFile() + } + + stop(): void { + if (this.timer) clearInterval(this.timer) + this.timer = null + } + + recent(): LogEvent[] { + return this.buffer + } + + appInfo(message: string): void { + this.push([this.makeEvent('app', 'info', message)]) + } + + get logsDir(): string { + return join(this.getBaseDir(), 'Logs') + } + + private todaysFile(): string { + const now = new Date() + const y = now.getFullYear() + const m = String(now.getMonth() + 1).padStart(2, '0') + const d = String(now.getDate()).padStart(2, '0') + return join(this.logsDir, `log_${y}-${m}-${d}.txt`) + } + + private async pollFile(): Promise { + try { + const file = this.todaysFile() + if (!existsSync(file)) { + if (this.currentFile === file) return + this.currentFile = null + return + } + + const stat = await fs.stat(file) + if (file !== this.currentFile) { + // New day or first poll: tail the end of the file rather than re-emitting history. + this.currentFile = file + this.offset = Math.max(0, stat.size - INITIAL_TAIL_BYTES) + this.pendingPartial = '' + } + if (stat.size < this.offset) { + // File truncated/rotated. + this.offset = 0 + this.pendingPartial = '' + } + if (stat.size === this.offset) return + + const handle = await fs.open(file, 'r') + try { + const length = stat.size - this.offset + const chunk = Buffer.alloc(Math.min(length, 1024 * 1024)) + const { bytesRead } = await handle.read(chunk, 0, chunk.length, this.offset) + this.offset += bytesRead + const text = this.pendingPartial + chunk.subarray(0, bytesRead).toString('utf8') + const lines = text.split(/\r?\n/) + this.pendingPartial = lines.pop() ?? '' + const events = lines + .map((l) => l.trim()) + .filter((l) => l.length > 0) + .map((line) => this.makeEvent('file', classify(line), line)) + if (events.length > 0) this.push(events) + } finally { + await handle.close() + } + } catch { + // Logs folder unreadable - silent, retry next poll. + } + } + + private capturePipeResult(result: PipeResult): void { + const events: LogEvent[] = [] + const head = result.command.split(' ')[0] + if (!result.ok) { + events.push(this.makeEvent('pipe', 'error', `${head} failed: ${result.error ?? 'unknown error'}`)) + } else { + events.push(this.makeEvent('pipe', 'info', `> ${result.command} (${result.durationMs}ms)`)) + for (const line of result.lines.slice(0, 200)) { + events.push(this.makeEvent('pipe', classify(line), line)) + } + } + this.push(events) + } + + private makeEvent(source: LogEvent['source'], severity: LogSeverity, message: string): LogEvent { + return { id: this.nextId++, timestamp: Date.now(), source, severity, message: message.slice(0, 2000) } + } + + private push(events: LogEvent[]): void { + this.buffer.push(...events) + if (this.buffer.length > RING_BUFFER_SIZE) { + this.buffer = this.buffer.slice(this.buffer.length - RING_BUFFER_SIZE) + } + this.emit('events', events) + } +} + +function classify(line: string): LogSeverity { + const lower = line.toLowerCase() + if (/\b(error|failed|failure|exception|crash)\b/.test(lower)) return 'error' + if (/\b(warn|warning)\b/.test(lower)) return 'warning' + if (/\b(debug|trace|verbose)\b/.test(lower)) return 'debug' + return 'info' +} diff --git a/VirtualDriverControl/src/main/services/pipe-client.ts b/VirtualDriverControl/src/main/services/pipe-client.ts new file mode 100644 index 00000000..36dcb8cb --- /dev/null +++ b/VirtualDriverControl/src/main/services/pipe-client.ts @@ -0,0 +1,308 @@ +import { execFile } from 'child_process' +import { EventEmitter } from 'events' +import { promises as fs } from 'fs' +import { createConnection, Socket } from 'net' +import { join } from 'path' +import { promisify } from 'util' +import { app } from 'electron' +import type { DriverLiveSettings, PipeResult, PipeToggleCommand } from '@shared/types' + +const execFileAsync = promisify(execFile) + +const PIPE_PATH = '\\\\.\\pipe\\MTTVirtualDisplayPipe' + +/** + * Commands the driver answers with data. These need the PowerShell round trip: + * the driver responds with WriteFile immediately followed by + * DisconnectNamedPipe, which discards anything the client has not read yet. + * Only a client with an overlapped read already pending in the kernel receives + * the data - Node's net stack reads too late by design, .NET ReadAsync works. + */ +const RESPONSE_COMMANDS = new Set(['GETSETTINGS', 'GETALLGPUS', 'GETASSIGNEDGPU', 'IDDCXVERSION']) + +/** Round-trip helper: arms an overlapped read before writing the command. */ +const HELPER_PS1 = `param([Parameter(Mandatory=$true)][string]$CommandB64, [int]$TimeoutMs = 10000) +$ErrorActionPreference = 'Stop' +try { if (-not (Test-Path '\\\\.\\pipe\\MTTVirtualDisplayPipe')) { exit 2 } } catch { } +$cmd = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($CommandB64)) +$pipe = New-Object System.IO.Pipes.NamedPipeClientStream('.', 'MTTVirtualDisplayPipe', [System.IO.Pipes.PipeDirection]::InOut, [System.IO.Pipes.PipeOptions]::Asynchronous) +try { + try { $pipe.Connect(4000) } catch { exit 3 } + $buf = New-Object byte[] 65536 + $mem = New-Object System.IO.MemoryStream + $read = $pipe.ReadAsync($buf, 0, $buf.Length) + Start-Sleep -Milliseconds 30 + $bytes = [System.Text.Encoding]::Unicode.GetBytes($cmd) + $pipe.Write($bytes, 0, $bytes.Length) + $pipe.Flush() + if (-not $read.Wait($TimeoutMs)) { exit 4 } + $n = 0 + try { $n = $read.Result } catch { $n = 0 } + while ($n -gt 0) { + $mem.Write($buf, 0, $n) + $read = $pipe.ReadAsync($buf, 0, $buf.Length) + if (-not $read.Wait(400)) { break } + try { $n = $read.Result } catch { $n = 0 } + } + [Console]::Out.Write([Convert]::ToBase64String($mem.ToArray())) + exit 0 +} catch { + exit 5 +} finally { + $pipe.Dispose() +} +` + +/** Commands that trigger an internal driver reload (heavyweight, 2-8s). */ +const RELOAD_COMMANDS = new Set([ + 'SETDISPLAYCOUNT', + 'SETGPU', + 'HDRPLUS', + 'SDR10', + 'CUSTOMEDID', + 'PREVENTSPOOF', + 'CEAOVERRIDE', + 'HARDWARECURSOR' +]) + +const DEFAULT_TIMEOUT_MS = 10_000 +const RELOAD_TIMEOUT_MS = 45_000 +const CONNECT_TIMEOUT_MS = 4_000 +/** Minimum spacing between reload-triggering commands (driver stability). */ +const RELOAD_COOLDOWN_MS = 3_000 + +export interface PipeSendOptions { + timeoutMs?: number + /** Suppress activity events (used by the heartbeat PING). */ + quiet?: boolean +} + +/** + * Client for \\.\pipe\MTTVirtualDisplayPipe. + * + * Protocol rules implemented here: + * - one-shot connection per command (driver disconnects after responding) + * - commands written as UTF-16LE, responses read until disconnect + * - responses decoded as UTF-8 except GETSETTINGS (UTF-16LE) + * - all commands fully serialized; reload-triggering commands get a cooldown + * - RELOAD_DRIVER is never sent (upstream undefined behavior, issue #351) + * - fire-and-forget commands go through a Node socket; the server closing the + * pipe right after reading the command (EPIPE) counts as success + * - response-bearing commands go through a PowerShell helper that arms an + * overlapped read before writing, otherwise the driver's write-then- + * disconnect pattern discards the response before it can be read + */ +export class PipeClient extends EventEmitter { + private queue: Promise = Promise.resolve() + private lastReloadFinishedAt = 0 + private helperPath: string | null = null + + /** Serialized send. Resolves with a PipeResult, never rejects. */ + send(command: string, options: PipeSendOptions = {}): Promise { + const run = this.queue.then(() => this.execute(command, options)) + this.queue = run.catch(() => undefined) + return run + } + + async ping(): Promise { + // Connect + write succeeding proves the driver's pipe server handled the + // command; the PONG reply itself is unreadable without the PS helper and + // not worth a powershell spawn every heartbeat. + const result = await this.send('PING', { timeoutMs: 3_000, quiet: true }) + return result.ok + } + + async setDisplayCount(count: number): Promise { + const n = Math.max(0, Math.min(99, Math.floor(count))) + return this.send(`SETDISPLAYCOUNT ${n}`) + } + + async setToggle(name: PipeToggleCommand, value: boolean): Promise { + return this.send(`${name} ${value ? 'true' : 'false'}`) + } + + async setGpu(friendlyName: string): Promise { + const clean = friendlyName.replace(/["\r\n]/g, '').slice(0, 100) + return this.send(`SETGPU "${clean}"`) + } + + async getDriverSettings(): Promise { + const result = await this.send('GETSETTINGS') + if (!result.ok) return null + const match = result.response.match(/SETTINGS\s+DEBUG=(true|false)\s+LOG=(true|false)/i) + if (!match) return null + return { debug: match[1].toLowerCase() === 'true', log: match[2].toLowerCase() === 'true' } + } + + private isReloadCommand(command: string): boolean { + const head = command.split(' ')[0].toUpperCase() + return RELOAD_COMMANDS.has(head) + } + + private async execute(command: string, options: PipeSendOptions): Promise { + const started = Date.now() + const reload = this.isReloadCommand(command) + const timeoutMs = options.timeoutMs ?? (reload ? RELOAD_TIMEOUT_MS : DEFAULT_TIMEOUT_MS) + + if (command.toUpperCase() === 'RELOAD_DRIVER') { + return this.finish(command, started, options, { + ok: false, + response: '', + error: 'RELOAD_DRIVER is blocked: it causes undefined behavior in the driver. Use SETDISPLAYCOUNT instead.' + }) + } + if (command.length > 127) { + return this.finish(command, started, options, { + ok: false, + response: '', + error: 'Command exceeds the 127 character pipe buffer limit.' + }) + } + + if (reload) { + const wait = this.lastReloadFinishedAt + RELOAD_COOLDOWN_MS - Date.now() + if (wait > 0) await delay(wait) + } + + try { + const head = command.split(' ')[0].toUpperCase() + const raw = RESPONSE_COMMANDS.has(head) + ? await this.roundTripPs(command, timeoutMs) + : await this.roundTrip(command, timeoutMs) + const isUtf16Response = command.toUpperCase() === 'GETSETTINGS' + const decoded = raw + .toString(isUtf16Response ? 'utf16le' : 'utf8') + .replace(/\0+/g, '') + .trim() + return this.finish(command, started, options, { ok: true, response: decoded }) + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + return this.finish(command, started, options, { ok: false, response: '', error: message }) + } finally { + if (reload) this.lastReloadFinishedAt = Date.now() + } + } + + private finish( + command: string, + started: number, + options: PipeSendOptions, + partial: { ok: boolean; response: string; error?: string } + ): PipeResult { + const result: PipeResult = { + command, + ok: partial.ok, + response: partial.response, + lines: partial.response.length > 0 ? partial.response.split(/\r?\n/).filter((l) => l.trim().length > 0) : [], + durationMs: Date.now() - started, + error: partial.error + } + if (!options.quiet) this.emit('result', result) + return result + } + + private roundTrip(command: string, timeoutMs: number): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = [] + let settled = false + let wrote = false + let socket: Socket | null = null + + const overallTimer = setTimeout(() => { + fail(new Error(`Pipe command timed out after ${Math.round(timeoutMs / 1000)}s`)) + }, timeoutMs) + + const succeed = (): void => { + if (settled) return + settled = true + clearTimeout(overallTimer) + socket?.destroy() + resolve(Buffer.concat(chunks)) + } + + const fail = (error: Error): void => { + if (settled) return + settled = true + clearTimeout(overallTimer) + socket?.destroy() + const friendly = + (error as NodeJS.ErrnoException).code === 'ENOENT' + ? new Error('Driver pipe not available (driver not running)') + : error + reject(friendly) + } + + socket = createConnection(PIPE_PATH) + socket.setTimeout(CONNECT_TIMEOUT_MS, () => { + // Only treat as failure while still connecting; once data flows we rely on the overall timer. + if (chunks.length === 0 && socket && socket.connecting) { + fail(new Error('Timed out connecting to driver pipe')) + } + }) + + socket.on('connect', () => { + socket?.setTimeout(0) + socket?.write(Buffer.from(command, 'utf16le'), (err) => { + if (err) fail(err) + else wrote = true + }) + }) + socket.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))) + socket.on('end', succeed) + socket.on('close', succeed) + socket.on('error', (error) => { + // The driver disconnects as soon as it has read the command, which + // surfaces as EPIPE here. The command was delivered - that's success. + if (wrote && (error as NodeJS.ErrnoException).code === 'EPIPE') succeed() + else fail(error) + }) + }) + } + + /** + * Response-bearing round trip via PowerShell/.NET: an overlapped ReadAsync + * is pending in the kernel before the command is written, so the response + * survives the driver's immediate DisconnectNamedPipe. Returns raw bytes + * (stdout carries them base64-encoded to avoid console encoding mangling). + */ + private async roundTripPs(command: string, timeoutMs: number): Promise { + const helper = await this.ensureHelper() + try { + const { stdout } = await execFileAsync( + 'powershell.exe', + [ + '-NoProfile', + '-NonInteractive', + '-ExecutionPolicy', + 'Bypass', + '-File', + helper, + '-CommandB64', + Buffer.from(command, 'utf8').toString('base64'), + '-TimeoutMs', + String(timeoutMs) + ], + { windowsHide: true, timeout: timeoutMs + 15_000, maxBuffer: 4 * 1024 * 1024 } + ) + return Buffer.from(stdout.trim(), 'base64') + } catch (error) { + const code = (error as { code?: number }).code + if (code === 2) throw new Error('Driver pipe not available (driver not running)') + if (code === 3) throw new Error('Timed out connecting to driver pipe') + if (code === 4) throw new Error(`Pipe command timed out after ${Math.round(timeoutMs / 1000)}s`) + throw error instanceof Error ? error : new Error(String(error)) + } + } + + private async ensureHelper(): Promise { + if (this.helperPath) return this.helperPath + const path = join(app.getPath('userData'), 'pipe-helper.ps1') + await fs.writeFile(path, HELPER_PS1, 'utf8') + this.helperPath = path + return path + } +} + +function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)) +} diff --git a/VirtualDriverControl/src/main/services/prefs-service.ts b/VirtualDriverControl/src/main/services/prefs-service.ts new file mode 100644 index 00000000..e4219ada --- /dev/null +++ b/VirtualDriverControl/src/main/services/prefs-service.ts @@ -0,0 +1,167 @@ +import { execFile } from 'child_process' +import { app } from 'electron' +import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs' +import { join } from 'path' +import { promisify } from 'util' +import { DEFAULT_BASE_DIR, type AppPreferences, type BaseDirResult } from '@shared/types' + +const execFileAsync = promisify(execFile) + +/** Registry location the driver reads its settings path from (confirmed in MttVDD.dll). */ +const VDD_REG_KEY = 'HKLM\\SOFTWARE\\MikeTheTech\\VirtualDisplayDriver' +const VDD_REG_VALUE = 'VDDPATH' + +/** Absolute Windows path without characters that would break quoting. */ +const SAFE_PATH_PATTERN = /^[A-Za-z]:\\[^"'<>|?*\r\n]*$/ + +const DEFAULT_PREFS: AppPreferences = { + theme: 'dark', + accent: '#4cc2ff', + baseDir: DEFAULT_BASE_DIR, + audioRoutes: [] +} + +export class PrefsService { + private prefs: AppPreferences + + constructor() { + this.prefs = this.read() + } + + get(): AppPreferences { + return this.prefs + } + + getBaseDir(): string { + return this.prefs.baseDir || DEFAULT_BASE_DIR + } + + set(patch: Partial): AppPreferences { + const next: AppPreferences = { ...this.prefs, ...patch } + if (typeof next.baseDir !== 'string' || next.baseDir.trim().length === 0) next.baseDir = DEFAULT_BASE_DIR + if (!/^#[0-9a-fA-F]{6}$/.test(next.accent)) next.accent = DEFAULT_PREFS.accent + if (!['dark', 'light', 'system'].includes(next.theme)) next.theme = 'dark' + if (!Array.isArray(next.audioRoutes)) next.audioRoutes = [] + next.audioRoutes = next.audioRoutes.slice(0, 16).filter( + (r) => typeof r?.id === 'string' && typeof r?.sourceId === 'string' && typeof r?.sinkId === 'string' + ) + this.prefs = next + this.write() + return this.prefs + } + + /** + * Reads the driver's VDDPATH registry value. The driver loads + * `\vdd_settings.xml`, defaulting to C:\VirtualDisplayDriver when + * the value is absent - so the app must follow it to edit the real file. + */ + async readDriverRegistryPath(): Promise { + try { + const { stdout } = await execFileAsync('reg.exe', ['query', VDD_REG_KEY, '/v', VDD_REG_VALUE], { + windowsHide: true, + timeout: 10_000 + }) + const match = new RegExp(`${VDD_REG_VALUE}\\s+REG_(?:EXPAND_)?SZ\\s+(.+)`).exec(stdout) + const value = match?.[1]?.trim() + return value && value.length > 0 ? value : null + } catch { + return null + } + } + + /** + * Aligns the app's base dir with what the driver will actually read: + * the VDDPATH registry value, or C:\VirtualDisplayDriver when unset. + */ + async syncBaseDirWithDriver(): Promise { + const effective = normalizePath((await this.readDriverRegistryPath()) ?? DEFAULT_BASE_DIR) + if (effective.toLowerCase() !== this.getBaseDir().toLowerCase()) { + this.set({ baseDir: effective }) + } + } + + /** + * Changes the driver folder: writes VDDPATH (directly when the app is + * elevated, otherwise through a single UAC prompt), verifies the registry + * took the value, and only then updates the preference - so the app and the + * driver can never point at different places. + */ + async setBaseDir(rawPath: string): Promise { + const baseDir = normalizePath(rawPath) + if (!SAFE_PATH_PATTERN.test(baseDir)) { + return { ok: false, prefs: this.prefs, error: 'Enter an absolute path like C:\\VirtualDisplayDriver' } + } + + if (!(await this.writeRegistryDirect(baseDir))) { + await this.writeRegistryElevated(baseDir) + } + + const applied = normalizePath((await this.readDriverRegistryPath()) ?? DEFAULT_BASE_DIR) + if (applied.toLowerCase() !== baseDir.toLowerCase()) { + // Registry still points elsewhere (UAC declined / write failed). + await this.syncBaseDirWithDriver() + return { + ok: false, + prefs: this.prefs, + error: 'The VDDPATH registry value could not be updated (elevation declined?). Folder left unchanged.' + } + } + + this.set({ baseDir }) + return { ok: true, prefs: this.prefs } + } + + private async writeRegistryDirect(baseDir: string): Promise { + try { + await execFileAsync('reg.exe', ['add', VDD_REG_KEY, '/v', VDD_REG_VALUE, '/t', 'REG_SZ', '/d', baseDir, '/f'], { + windowsHide: true, + timeout: 10_000 + }) + return true + } catch { + return false + } + } + + private async writeRegistryElevated(baseDir: string): Promise { + const launcher = `$p = Start-Process reg.exe -Verb RunAs -Wait -PassThru -WindowStyle Hidden -ArgumentList 'add','${VDD_REG_KEY}','/v','${VDD_REG_VALUE}','/t','REG_SZ','/d','"${baseDir}"','/f'; exit $p.ExitCode` + try { + await execFileAsync('powershell.exe', ['-NoProfile', '-Command', launcher], { windowsHide: true, timeout: 120_000 }) + } catch { + // verified by re-reading the registry afterwards + } + } + + private get filePath(): string { + return join(app.getPath('userData'), 'preferences.json') + } + + private read(): AppPreferences { + try { + if (existsSync(this.filePath)) { + const parsed = JSON.parse(readFileSync(this.filePath, 'utf8')) as Partial + // Migrate the pre-WinUI default accent (mint) to the new default. + if (parsed.accent === '#36c98e') delete parsed.accent + return { ...DEFAULT_PREFS, ...parsed } + } + } catch { + // fall through to defaults + } + return { ...DEFAULT_PREFS } + } + + private write(): void { + try { + mkdirSync(app.getPath('userData'), { recursive: true }) + writeFileSync(this.filePath, JSON.stringify(this.prefs, null, 2), 'utf8') + } catch { + // non-fatal + } + } +} + +/** Trims and strips a trailing backslash (keeps drive roots like C:\ intact). */ +function normalizePath(value: string): string { + const trimmed = value.trim() + return /^[A-Za-z]:\\$/.test(trimmed) ? trimmed : trimmed.replace(/[\\/]+$/, '') +} diff --git a/VirtualDriverControl/src/main/services/settings-service.ts b/VirtualDriverControl/src/main/services/settings-service.ts new file mode 100644 index 00000000..3edf66f7 --- /dev/null +++ b/VirtualDriverControl/src/main/services/settings-service.ts @@ -0,0 +1,516 @@ +import { XMLParser } from 'fast-xml-parser' +import { promises as fs } from 'fs' +import { existsSync, mkdirSync } from 'fs' +import { join } from 'path' +import { DEFAULT_VDD_SETTINGS } from '@shared/defaults' +import type { BackupInfo, ResolutionEntry, SaveResult, SettingsLoadResult, VddSettings } from '@shared/types' + +const MAX_BACKUPS = 20 + +type Raw = Record + +function toBool(value: unknown, fallback: boolean): boolean { + if (typeof value === 'string') return value.trim().toLowerCase() === 'true' + if (typeof value === 'boolean') return value + return fallback +} + +function toNum(value: unknown, fallback: number): number { + if (typeof value === 'number' && Number.isFinite(value)) return value + if (typeof value === 'string') { + const n = Number(value.trim()) + if (Number.isFinite(n)) return n + } + return fallback +} + +function toStr(value: unknown, fallback: string): string { + if (typeof value === 'string' && value.trim().length > 0) return value.trim() + if (typeof value === 'number') return String(value) + return fallback +} + +function asRaw(value: unknown): Raw { + return typeof value === 'object' && value !== null ? (value as Raw) : {} +} + +function asArray(value: unknown): unknown[] { + if (value === undefined || value === null) return [] + return Array.isArray(value) ? value : [value] +} + +function formatRate(rate: number): string { + return Number.isInteger(rate) ? String(rate) : String(Math.round(rate * 1000) / 1000) +} + +export class SettingsService { + constructor(private readonly getBaseDir: () => string) {} + + get settingsPath(): string { + return join(this.getBaseDir(), 'vdd_settings.xml') + } + + get backupsDir(): string { + return join(this.getBaseDir(), 'Backups') + } + + get edidDir(): string { + return join(this.getBaseDir(), 'EDID') + } + + /** + * Creates the driver folder and a default vdd_settings.xml when missing, + * so the driver always finds a valid configuration at its lookup path. + */ + async ensureDefaults(): Promise { + try { + mkdirSync(this.getBaseDir(), { recursive: true }) + if (!existsSync(this.settingsPath)) { + await fs.writeFile(this.settingsPath, this.serialize(DEFAULT_VDD_SETTINGS)) + } + } catch { + // folder not writable without elevation - app keeps working offline + } + } + + async load(): Promise { + try { + if (!existsSync(this.settingsPath)) { + return { ok: true, settings: structuredClone(DEFAULT_VDD_SETTINGS), isDefault: true } + } + const rawXml = await fs.readFile(this.settingsPath, 'utf8') + const settings = this.parse(rawXml) + return { ok: true, settings, rawXml, isDefault: false } + } catch (error) { + return { + ok: false, + isDefault: false, + error: error instanceof Error ? error.message : String(error) + } + } + } + + async rawXml(): Promise { + try { + return await fs.readFile(this.settingsPath, 'utf8') + } catch { + return null + } + } + + async save(settings: VddSettings): Promise { + try { + const xml = this.serialize(settings) + const backupCreated = await this.backupCurrent() + const tmpPath = `${this.settingsPath}.tmp` + mkdirSync(this.getBaseDir(), { recursive: true }) + await fs.writeFile(tmpPath, xml, 'utf8') + await fs.rm(this.settingsPath, { force: true }) + await fs.rename(tmpPath, this.settingsPath) + return { ok: true, backupCreated } + } catch (error) { + return { ok: false, error: error instanceof Error ? error.message : String(error) } + } + } + + async listBackups(): Promise { + try { + const entries = await fs.readdir(this.backupsDir) + const backups: BackupInfo[] = [] + for (const fileName of entries) { + if (!/^vdd_settings_[\d\-_]+\.xml$/.test(fileName)) continue + const fullPath = join(this.backupsDir, fileName) + const stat = await fs.stat(fullPath) + backups.push({ fileName, fullPath, createdAt: stat.mtimeMs, sizeBytes: stat.size }) + } + return backups.sort((a, b) => b.createdAt - a.createdAt) + } catch { + return [] + } + } + + async restoreBackup(fileName: string): Promise { + try { + if (!/^vdd_settings_[\d\-_]+\.xml$/.test(fileName)) { + return { ok: false, error: 'Invalid backup file name' } + } + const source = join(this.backupsDir, fileName) + if (!existsSync(source)) return { ok: false, error: 'Backup not found' } + const backupCreated = await this.backupCurrent() + await fs.copyFile(source, this.settingsPath) + return { ok: true, backupCreated } + } catch (error) { + return { ok: false, error: error instanceof Error ? error.message : String(error) } + } + } + + async saveMonitorProfile(profileXml: string, edidBytes?: Uint8Array): Promise { + try { + mkdirSync(this.edidDir, { recursive: true }) + await fs.writeFile(join(this.edidDir, 'monitor_profile.xml'), profileXml, 'utf8') + if (edidBytes && edidBytes.length >= 128) { + await fs.writeFile(join(this.getBaseDir(), 'user_edid.bin'), Buffer.from(edidBytes)) + } + return { ok: true } + } catch (error) { + return { ok: false, error: error instanceof Error ? error.message : String(error) } + } + } + + private async backupCurrent(): Promise { + if (!existsSync(this.settingsPath)) return undefined + mkdirSync(this.backupsDir, { recursive: true }) + const stamp = new Date() + .toISOString() + .replace(/[:T]/g, '-') + .replace(/\..+/, '') + const fileName = `vdd_settings_${stamp}.xml` + await fs.copyFile(this.settingsPath, join(this.backupsDir, fileName)) + await this.pruneBackups() + return fileName + } + + private async pruneBackups(): Promise { + const backups = await this.listBackups() + for (const old of backups.slice(MAX_BACKUPS)) { + try { + await fs.rm(old.fullPath, { force: true }) + } catch { + // best effort + } + } + } + + // ------------------------------------------------------------------------- + // XML <-> model + // ------------------------------------------------------------------------- + + parse(xml: string): VddSettings { + const parser = new XMLParser({ + ignoreAttributes: true, + parseTagValue: false, + trimValues: true, + isArray: (name) => name === 'resolution' || name === 'g_refresh_rate' || name === 'refresh_rate' + }) + const doc = asRaw(asRaw(parser.parse(xml)).vdd_settings) + const d = DEFAULT_VDD_SETTINGS + + const globalNode = asRaw(doc.global) + const resolutionsNode = asRaw(doc.resolutions) + const loggingNode = asRaw(doc.logging) + const colourNode = asRaw(doc.colour) + const cursorNode = asRaw(doc.cursor) + const edidNode = asRaw(doc.edid) + const integrationNode = asRaw(doc.edid_integration) + const hdrNode = asRaw(doc.hdr_advanced) + const hdr10Node = asRaw(hdrNode.hdr10_static_metadata) + const primariesNode = asRaw(hdrNode.color_primaries) + const colorSpaceNode = asRaw(hdrNode.color_space) + const autoNode = asRaw(doc.auto_resolutions) + const filterNode = asRaw(autoNode.edid_mode_filtering) + const preferredNode = asRaw(autoNode.preferred_mode) + const advNode = asRaw(doc.color_advanced) + const bitDepthNode = asRaw(advNode.bit_depth_management) + const extFormatNode = asRaw(advNode.color_format_extended) + + const resolutions: ResolutionEntry[] = asArray(resolutionsNode.resolution) + .map((node) => { + const r = asRaw(node) + const rates = asArray(r.refresh_rate) + .map((v) => toNum(v, NaN)) + .filter((v) => Number.isFinite(v) && v > 0) + return { + width: toNum(r.width, 0), + height: toNum(r.height, 0), + refreshRates: rates.length > 0 ? rates : [60] + } + }) + .filter((r) => r.width > 0 && r.height > 0) + + const colourFormatRaw = toStr(colourNode.ColourFormat, d.colour.colourFormat) + const colourFormat = (['RGB', 'YCbCr444', 'YCbCr422', 'YCbCr420'] as const).find( + (f) => f.toLowerCase() === colourFormatRaw.toLowerCase() + ) + + return { + monitors: { count: toNum(asRaw(doc.monitors).count, d.monitors.count) }, + gpu: { friendlyName: toStr(asRaw(doc.gpu).friendlyname, d.gpu.friendlyName) }, + global: { + refreshRates: asArray(globalNode.g_refresh_rate) + .map((v) => toNum(v, NaN)) + .filter((v) => Number.isFinite(v) && v > 0) + }, + resolutions: resolutions.length > 0 ? resolutions : structuredClone(d.resolutions), + logging: { + sendLogsThroughPipe: toBool(loggingNode.SendLogsThroughPipe, d.logging.sendLogsThroughPipe), + logging: toBool(loggingNode.logging, d.logging.logging), + debugLogging: toBool(loggingNode.debuglogging, d.logging.debugLogging) + }, + colour: { + sdr10bit: toBool(colourNode.SDR10bit, d.colour.sdr10bit), + hdrPlus: toBool(colourNode.HDRPlus, d.colour.hdrPlus), + colourFormat: colourFormat ?? d.colour.colourFormat + }, + cursor: { + hardwareCursor: toBool(cursorNode.HardwareCursor, d.cursor.hardwareCursor), + cursorMaxX: toNum(cursorNode.CursorMaxX, d.cursor.cursorMaxX), + cursorMaxY: toNum(cursorNode.CursorMaxY, d.cursor.cursorMaxY), + alphaCursorSupport: toBool(cursorNode.AlphaCursorSupport, d.cursor.alphaCursorSupport), + xorCursorSupportLevel: toNum(cursorNode.XorCursorSupportLevel, d.cursor.xorCursorSupportLevel) + }, + edid: { + customEdid: toBool(edidNode.CustomEdid, d.edid.customEdid), + preventSpoof: toBool(edidNode.PreventSpoof, d.edid.preventSpoof), + edidCeaOverride: toBool(edidNode.EdidCeaOverride, d.edid.edidCeaOverride) + }, + edidIntegration: { + enabled: toBool(integrationNode.enabled, d.edidIntegration.enabled), + autoConfigureFromEdid: toBool(integrationNode.auto_configure_from_edid, d.edidIntegration.autoConfigureFromEdid), + edidProfilePath: toStr(integrationNode.edid_profile_path, d.edidIntegration.edidProfilePath), + overrideManualSettings: toBool(integrationNode.override_manual_settings, d.edidIntegration.overrideManualSettings), + fallbackOnError: toBool(integrationNode.fallback_on_error, d.edidIntegration.fallbackOnError) + }, + hdrAdvanced: { + hdr10StaticMetadata: { + enabled: toBool(hdr10Node.enabled, d.hdrAdvanced.hdr10StaticMetadata.enabled), + maxDisplayMasteringLuminance: toNum( + hdr10Node.max_display_mastering_luminance, + d.hdrAdvanced.hdr10StaticMetadata.maxDisplayMasteringLuminance + ), + minDisplayMasteringLuminance: toNum( + hdr10Node.min_display_mastering_luminance, + d.hdrAdvanced.hdr10StaticMetadata.minDisplayMasteringLuminance + ), + maxContentLightLevel: toNum(hdr10Node.max_content_light_level, d.hdrAdvanced.hdr10StaticMetadata.maxContentLightLevel), + maxFrameAvgLightLevel: toNum( + hdr10Node.max_frame_avg_light_level, + d.hdrAdvanced.hdr10StaticMetadata.maxFrameAvgLightLevel + ) + }, + colorPrimaries: { + enabled: toBool(primariesNode.enabled, d.hdrAdvanced.colorPrimaries.enabled), + redX: toNum(primariesNode.red_x, d.hdrAdvanced.colorPrimaries.redX), + redY: toNum(primariesNode.red_y, d.hdrAdvanced.colorPrimaries.redY), + greenX: toNum(primariesNode.green_x, d.hdrAdvanced.colorPrimaries.greenX), + greenY: toNum(primariesNode.green_y, d.hdrAdvanced.colorPrimaries.greenY), + blueX: toNum(primariesNode.blue_x, d.hdrAdvanced.colorPrimaries.blueX), + blueY: toNum(primariesNode.blue_y, d.hdrAdvanced.colorPrimaries.blueY), + whiteX: toNum(primariesNode.white_x, d.hdrAdvanced.colorPrimaries.whiteX), + whiteY: toNum(primariesNode.white_y, d.hdrAdvanced.colorPrimaries.whiteY) + }, + colorSpace: { + enabled: toBool(colorSpaceNode.enabled, d.hdrAdvanced.colorSpace.enabled), + gammaCorrection: toNum(colorSpaceNode.gamma_correction, d.hdrAdvanced.colorSpace.gammaCorrection), + primaryColorSpace: toStr(colorSpaceNode.primary_color_space, d.hdrAdvanced.colorSpace.primaryColorSpace), + enableMatrixTransform: toBool(colorSpaceNode.enable_matrix_transform, d.hdrAdvanced.colorSpace.enableMatrixTransform) + } + }, + autoResolutions: { + enabled: toBool(autoNode.enabled, d.autoResolutions.enabled), + sourcePriority: toStr(autoNode.source_priority, d.autoResolutions.sourcePriority), + edidModeFiltering: { + minRefreshRate: toNum(filterNode.min_refresh_rate, d.autoResolutions.edidModeFiltering.minRefreshRate), + maxRefreshRate: toNum(filterNode.max_refresh_rate, d.autoResolutions.edidModeFiltering.maxRefreshRate), + excludeFractionalRates: toBool( + filterNode.exclude_fractional_rates, + d.autoResolutions.edidModeFiltering.excludeFractionalRates + ), + minResolutionWidth: toNum(filterNode.min_resolution_width, d.autoResolutions.edidModeFiltering.minResolutionWidth), + minResolutionHeight: toNum(filterNode.min_resolution_height, d.autoResolutions.edidModeFiltering.minResolutionHeight), + maxResolutionWidth: toNum(filterNode.max_resolution_width, d.autoResolutions.edidModeFiltering.maxResolutionWidth), + maxResolutionHeight: toNum(filterNode.max_resolution_height, d.autoResolutions.edidModeFiltering.maxResolutionHeight) + }, + preferredMode: { + useEdidPreferred: toBool(preferredNode.use_edid_preferred, d.autoResolutions.preferredMode.useEdidPreferred), + fallbackWidth: toNum(preferredNode.fallback_width, d.autoResolutions.preferredMode.fallbackWidth), + fallbackHeight: toNum(preferredNode.fallback_height, d.autoResolutions.preferredMode.fallbackHeight), + fallbackRefresh: toNum(preferredNode.fallback_refresh, d.autoResolutions.preferredMode.fallbackRefresh) + } + }, + colorAdvanced: { + bitDepthManagement: { + autoSelectFromColorSpace: toBool( + bitDepthNode.auto_select_from_color_space, + d.colorAdvanced.bitDepthManagement.autoSelectFromColorSpace + ), + forceBitDepth: toNum(bitDepthNode.force_bit_depth, d.colorAdvanced.bitDepthManagement.forceBitDepth), + fp16SurfaceSupport: toBool(bitDepthNode.fp16_surface_support, d.colorAdvanced.bitDepthManagement.fp16SurfaceSupport) + }, + colorFormatExtended: { + sdrWhiteLevel: toNum(extFormatNode.sdr_white_level, d.colorAdvanced.colorFormatExtended.sdrWhiteLevel) + } + } + } + } + + serialize(s: VddSettings): string { + const b = (v: boolean): string => (v ? 'true' : 'false') + const lines: string[] = [] + lines.push(``) + lines.push(``) + lines.push(``) + lines.push(``) + lines.push(` `) + lines.push(` `) + lines.push(` ${Math.max(0, Math.min(99, Math.floor(s.monitors.count)))}`) + lines.push(` `) + lines.push(``) + lines.push(` `) + lines.push(` ${escapeXml(s.gpu.friendlyName || 'default')}`) + lines.push(` `) + lines.push(``) + lines.push(` `) + lines.push(` `) + for (const rate of s.global.refreshRates) { + lines.push(` ${formatRate(rate)}`) + } + lines.push(` `) + lines.push(``) + lines.push(` `) + for (const res of s.resolutions) { + lines.push(` `) + lines.push(` ${Math.floor(res.width)}`) + lines.push(` ${Math.floor(res.height)}`) + for (const rate of res.refreshRates) { + lines.push(` ${formatRate(rate)}`) + } + lines.push(` `) + } + lines.push(` `) + lines.push(``) + lines.push(` `) + lines.push(` `) + lines.push(` ${b(s.logging.sendLogsThroughPipe)}`) + lines.push(` ${b(s.logging.logging)}`) + lines.push(` ${b(s.logging.debugLogging)}`) + lines.push(` `) + lines.push(``) + lines.push(` `) + lines.push(` `) + lines.push(` ${b(s.colour.sdr10bit)}`) + lines.push(` ${b(s.colour.hdrPlus)}`) + lines.push(` ${s.colour.colourFormat}`) + lines.push(` `) + lines.push(``) + lines.push(` `) + lines.push(` `) + lines.push(` ${b(s.cursor.hardwareCursor)}`) + lines.push(` ${Math.floor(s.cursor.cursorMaxX)}`) + lines.push(` ${Math.floor(s.cursor.cursorMaxY)}`) + lines.push(` ${b(s.cursor.alphaCursorSupport)}`) + lines.push(` ${Math.floor(s.cursor.xorCursorSupportLevel)}`) + lines.push(` `) + lines.push(``) + lines.push(` `) + lines.push(` `) + lines.push(` ${b(s.edid.customEdid)}`) + lines.push(` ${b(s.edid.preventSpoof)}`) + lines.push(` ${b(s.edid.edidCeaOverride)}`) + lines.push(` `) + lines.push(``) + lines.push(` `) + lines.push(` `) + lines.push(` ${b(s.edidIntegration.enabled)}`) + lines.push(` ${b(s.edidIntegration.autoConfigureFromEdid)}`) + lines.push(` ${escapeXml(s.edidIntegration.edidProfilePath)}`) + lines.push(` ${b(s.edidIntegration.overrideManualSettings)}`) + lines.push(` ${b(s.edidIntegration.fallbackOnError)}`) + lines.push(` `) + lines.push(``) + lines.push(` `) + lines.push(` `) + lines.push(` `) + lines.push(` ${b(s.hdrAdvanced.hdr10StaticMetadata.enabled)}`) + lines.push( + ` ${s.hdrAdvanced.hdr10StaticMetadata.maxDisplayMasteringLuminance.toFixed(1)}` + ) + lines.push( + ` ${s.hdrAdvanced.hdr10StaticMetadata.minDisplayMasteringLuminance}` + ) + lines.push( + ` ${Math.floor(s.hdrAdvanced.hdr10StaticMetadata.maxContentLightLevel)}` + ) + lines.push( + ` ${Math.floor(s.hdrAdvanced.hdr10StaticMetadata.maxFrameAvgLightLevel)}` + ) + lines.push(` `) + lines.push(` `) + lines.push(` ${b(s.hdrAdvanced.colorPrimaries.enabled)}`) + lines.push(` ${s.hdrAdvanced.colorPrimaries.redX.toFixed(3)}`) + lines.push(` ${s.hdrAdvanced.colorPrimaries.redY.toFixed(3)}`) + lines.push(` ${s.hdrAdvanced.colorPrimaries.greenX.toFixed(3)}`) + lines.push(` ${s.hdrAdvanced.colorPrimaries.greenY.toFixed(3)}`) + lines.push(` ${s.hdrAdvanced.colorPrimaries.blueX.toFixed(3)}`) + lines.push(` ${s.hdrAdvanced.colorPrimaries.blueY.toFixed(3)}`) + lines.push(` ${s.hdrAdvanced.colorPrimaries.whiteX.toFixed(4)}`) + lines.push(` ${s.hdrAdvanced.colorPrimaries.whiteY.toFixed(4)}`) + lines.push(` `) + lines.push(` `) + lines.push(` ${b(s.hdrAdvanced.colorSpace.enabled)}`) + lines.push(` ${s.hdrAdvanced.colorSpace.gammaCorrection}`) + lines.push(` ${escapeXml(s.hdrAdvanced.colorSpace.primaryColorSpace)}`) + lines.push(` ${b(s.hdrAdvanced.colorSpace.enableMatrixTransform)}`) + lines.push(` `) + lines.push(` `) + lines.push(``) + lines.push(` `) + lines.push(` `) + lines.push(` ${b(s.autoResolutions.enabled)}`) + lines.push(` ${escapeXml(s.autoResolutions.sourcePriority)}`) + lines.push(` `) + lines.push(` ${formatRate(s.autoResolutions.edidModeFiltering.minRefreshRate)}`) + lines.push(` ${formatRate(s.autoResolutions.edidModeFiltering.maxRefreshRate)}`) + lines.push( + ` ${b(s.autoResolutions.edidModeFiltering.excludeFractionalRates)}` + ) + lines.push( + ` ${Math.floor(s.autoResolutions.edidModeFiltering.minResolutionWidth)}` + ) + lines.push( + ` ${Math.floor(s.autoResolutions.edidModeFiltering.minResolutionHeight)}` + ) + lines.push( + ` ${Math.floor(s.autoResolutions.edidModeFiltering.maxResolutionWidth)}` + ) + lines.push( + ` ${Math.floor(s.autoResolutions.edidModeFiltering.maxResolutionHeight)}` + ) + lines.push(` `) + lines.push(` `) + lines.push(` ${b(s.autoResolutions.preferredMode.useEdidPreferred)}`) + lines.push(` ${Math.floor(s.autoResolutions.preferredMode.fallbackWidth)}`) + lines.push(` ${Math.floor(s.autoResolutions.preferredMode.fallbackHeight)}`) + lines.push(` ${formatRate(s.autoResolutions.preferredMode.fallbackRefresh)}`) + lines.push(` `) + lines.push(` `) + lines.push(``) + lines.push(` `) + lines.push(` `) + lines.push(` `) + lines.push( + ` ${b(s.colorAdvanced.bitDepthManagement.autoSelectFromColorSpace)}` + ) + lines.push(` ${Math.floor(s.colorAdvanced.bitDepthManagement.forceBitDepth)}`) + lines.push( + ` ${b(s.colorAdvanced.bitDepthManagement.fp16SurfaceSupport)}` + ) + lines.push(` `) + lines.push(` `) + lines.push(` ${s.colorAdvanced.colorFormatExtended.sdrWhiteLevel.toFixed(1)}`) + lines.push(` `) + lines.push(` `) + lines.push(``) + lines.push(``) + lines.push(``) + return lines.join('\n') + } +} + +function escapeXml(value: string): string { + return value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') +} diff --git a/VirtualDriverControl/src/preload/index.d.ts b/VirtualDriverControl/src/preload/index.d.ts new file mode 100644 index 00000000..8e25ca9c --- /dev/null +++ b/VirtualDriverControl/src/preload/index.d.ts @@ -0,0 +1,9 @@ +import type { VddApi } from './index' + +declare global { + interface Window { + vdd: VddApi + } +} + +export {} diff --git a/VirtualDriverControl/src/preload/index.ts b/VirtualDriverControl/src/preload/index.ts new file mode 100644 index 00000000..ec43af36 --- /dev/null +++ b/VirtualDriverControl/src/preload/index.ts @@ -0,0 +1,113 @@ +import { contextBridge, ipcRenderer } from 'electron' +import type { + AppPreferences, + AudioEndpoint, + BackupInfo, + BaseDirResult, + DisplayLayoutInfo, + DriverLiveSettings, + DriverStatus, + GpuInfo, + InstallProgress, + LifecycleResult, + LogEvent, + ManagedDeviceState, + ManagedDriverId, + PipeActivity, + PipeResult, + PipeToggleCommand, + ReleaseInfo, + SaveResult, + SettingsLoadResult, + SystemInfo, + VddSettings +} from '@shared/types' + +function subscribe(channel: string, callback: (payload: T) => void): () => void { + const listener = (_event: Electron.IpcRendererEvent, payload: T): void => callback(payload) + ipcRenderer.on(channel, listener) + return () => ipcRenderer.removeListener(channel, listener) +} + +const api = { + env: { + /** Window backdrop chosen by the main process (Mica on Win11 22H2+). */ + backdrop: (process.argv.includes('--vdd-backdrop=mica') ? 'mica' : 'solid') as 'mica' | 'solid' + }, + pipe: { + ping: (): Promise => ipcRenderer.invoke('pipe:ping'), + setDisplayCount: (count: number): Promise => ipcRenderer.invoke('pipe:set-display-count', count), + toggle: (name: PipeToggleCommand, value: boolean): Promise => ipcRenderer.invoke('pipe:toggle', name, value), + setGpu: (name: string): Promise => ipcRenderer.invoke('pipe:set-gpu', name), + query: (command: string): Promise => ipcRenderer.invoke('pipe:query', command), + sendRaw: (command: string): Promise => ipcRenderer.invoke('pipe:send-raw', command), + getDriverSettings: (): Promise => ipcRenderer.invoke('pipe:get-driver-settings') + }, + settings: { + load: (): Promise => ipcRenderer.invoke('settings:load'), + save: (settings: VddSettings): Promise => ipcRenderer.invoke('settings:save', settings), + preview: (settings: VddSettings): Promise => ipcRenderer.invoke('settings:preview', settings), + raw: (): Promise => ipcRenderer.invoke('settings:raw'), + backups: (): Promise => ipcRenderer.invoke('settings:backups'), + restore: (fileName: string): Promise => ipcRenderer.invoke('settings:restore', fileName), + saveMonitorProfile: (xml: string, edidBytes?: Uint8Array): Promise => + ipcRenderer.invoke('settings:save-monitor-profile', xml, edidBytes) + }, + driver: { + status: (force?: boolean): Promise => ipcRenderer.invoke('driver:status', force), + gpus: (): Promise => ipcRenderer.invoke('driver:gpus'), + iddcxVersion: (): Promise => ipcRenderer.invoke('driver:iddcx-version') + }, + installer: { + latestRelease: (driver: ManagedDriverId): Promise => ipcRenderer.invoke('installer:latest-release', driver), + installedTag: (driver: ManagedDriverId): Promise => ipcRenderer.invoke('installer:installed-tag', driver), + deviceState: (driver: ManagedDriverId): Promise => ipcRenderer.invoke('installer:device-state', driver), + install: (driver: ManagedDriverId, instances?: number): Promise => + ipcRenderer.invoke('installer:install', driver, instances), + uninstall: (driver: ManagedDriverId): Promise => ipcRenderer.invoke('installer:uninstall', driver), + restartDevice: (driver: ManagedDriverId): Promise => ipcRenderer.invoke('installer:restart-device', driver), + setInstances: (driver: ManagedDriverId, count: number): Promise => + ipcRenderer.invoke('installer:set-instances', driver, count), + /** Boot-config test signing state - the audio driver is test-signed and needs it. */ + testSigning: (): Promise => ipcRenderer.invoke('installer:test-signing'), + setTestSigning: (enabled: boolean): Promise => ipcRenderer.invoke('installer:set-test-signing', enabled) + }, + audio: { + endpoints: (): Promise => ipcRenderer.invoke('audio:endpoints'), + setDefault: (id: string): Promise => ipcRenderer.invoke('audio:set-default', id), + setVolume: (id: string, volume: number): Promise => ipcRenderer.invoke('audio:set-volume', id, volume), + setMute: (id: string, muted: boolean): Promise => ipcRenderer.invoke('audio:set-mute', id, muted) + }, + system: { + info: (): Promise => ipcRenderer.invoke('system:info'), + displays: (): Promise => ipcRenderer.invoke('system:displays'), + openExternal: (url: string): Promise => ipcRenderer.invoke('shell:open-external', url), + openPath: (which: 'base' | 'logs' | 'backups' | 'edid'): Promise => ipcRenderer.invoke('shell:open-path', which) + }, + logs: { + recent: (): Promise => ipcRenderer.invoke('logs:recent') + }, + prefs: { + get: (): Promise => ipcRenderer.invoke('prefs:get'), + set: (patch: Partial): Promise => ipcRenderer.invoke('prefs:set', patch), + /** Changes the driver folder - updates the VDDPATH registry value and the preference atomically. */ + setBaseDir: (baseDir: string): Promise => ipcRenderer.invoke('prefs:set-base-dir', baseDir) + }, + window: { + minimize: (): void => ipcRenderer.send('window:minimize'), + maximizeToggle: (): void => ipcRenderer.send('window:maximize-toggle'), + close: (): void => ipcRenderer.send('window:close') + }, + events: { + onStatus: (cb: (status: DriverStatus) => void): (() => void) => subscribe('push:status', cb), + onLogs: (cb: (events: LogEvent[]) => void): (() => void) => subscribe('push:logs', cb), + onPipeActivity: (cb: (activity: PipeActivity) => void): (() => void) => subscribe('push:pipe-activity', cb), + onMaximized: (cb: (maximized: boolean) => void): (() => void) => subscribe('push:maximized', cb), + onInstallProgress: (cb: (progress: InstallProgress) => void): (() => void) => subscribe('push:install-progress', cb), + onDisplays: (cb: (layout: DisplayLayoutInfo[]) => void): (() => void) => subscribe('push:displays', cb) + } +} + +export type VddApi = typeof api + +contextBridge.exposeInMainWorld('vdd', api) diff --git a/VirtualDriverControl/src/renderer/index.html b/VirtualDriverControl/src/renderer/index.html new file mode 100644 index 00000000..ccaf7a2f --- /dev/null +++ b/VirtualDriverControl/src/renderer/index.html @@ -0,0 +1,16 @@ + + + + + Virtual Driver Control + + + + +
+ + + diff --git a/VirtualDriverControl/src/renderer/src/App.tsx b/VirtualDriverControl/src/renderer/src/App.tsx new file mode 100644 index 00000000..4439e137 --- /dev/null +++ b/VirtualDriverControl/src/renderer/src/App.tsx @@ -0,0 +1,72 @@ +import { useEffect } from 'react' +import { AnimatePresence, motion } from 'motion/react' +import { SaveBar } from './components/SaveBar' +import { Sidebar } from './components/Sidebar' +import { TitleBar } from './components/TitleBar' +import { Toasts } from './components/Toasts' +import { AudioPage } from './pages/AudioPage' +import { ConsolePage } from './pages/ConsolePage' +import { ColorPage } from './pages/ColorPage' +import { DashboardPage } from './pages/DashboardPage' +import { DisplaysPage } from './pages/DisplaysPage' +import { EdidPage } from './pages/EdidPage' +import { GpuPage } from './pages/GpuPage' +import { SettingsPage } from './pages/SettingsPage' +import { useAudio } from './stores/audio' +import { useDriver } from './stores/driver' +import { useLogs } from './stores/logs' +import { useSettings } from './stores/settings' +import { useUi, type PageId } from './stores/ui' + +const PAGES: Record React.JSX.Element> = { + dashboard: DashboardPage, + displays: DisplaysPage, + color: ColorPage, + edid: EdidPage, + gpu: GpuPage, + audio: AudioPage, + console: ConsolePage, + settings: SettingsPage +} + +export default function App(): React.JSX.Element { + const page = useUi((s) => s.page) + const initPrefs = useUi((s) => s.initPrefs) + const setMaximized = useUi((s) => s.setMaximized) + + useEffect(() => { + document.body.dataset.backdrop = window.vdd.env.backdrop + // Audio init waits for prefs so saved routes can be re-armed. + void initPrefs().then(() => useAudio.getState().init()) + void useDriver.getState().init() + void useSettings.getState().load() + void useLogs.getState().init() + const unsubscribe = window.vdd.events.onMaximized(setMaximized) + return unsubscribe + }, [initPrefs, setMaximized]) + + const Page = PAGES[page] + + return ( +
+ + +
+ + + + + + +
+ +
+ ) +} diff --git a/VirtualDriverControl/src/renderer/src/assets/logo-err.png b/VirtualDriverControl/src/renderer/src/assets/logo-err.png new file mode 100644 index 0000000000000000000000000000000000000000..b729a35b085c3c114032342ffb433b17c82324d9 GIT binary patch literal 24009 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 a@n1g6Z?*lqip?GjK;Y@>=d#Wzp$P!;upgoT literal 0 HcmV?d00001 diff --git a/VirtualDriverControl/src/renderer/src/assets/logo-warn.png b/VirtualDriverControl/src/renderer/src/assets/logo-warn.png new file mode 100644 index 0000000000000000000000000000000000000000..fe8a27551f515b28625a83830fc7484ffe14ba98 GIT binary patch literal 31041 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 literal 0 HcmV?d00001 diff --git a/VirtualDriverControl/src/renderer/src/components/ArrangementMap.tsx b/VirtualDriverControl/src/renderer/src/components/ArrangementMap.tsx new file mode 100644 index 00000000..5f971ee2 --- /dev/null +++ b/VirtualDriverControl/src/renderer/src/components/ArrangementMap.tsx @@ -0,0 +1,184 @@ +import { useEffect, useRef, useState } from 'react' +import { Loader2, Minus, MonitorCheck, Plus, RefreshCw, Star } from 'lucide-react' +import type { DisplayLayoutInfo } from '@shared/types' +import { Card } from '@renderer/components/ui' +import { useDriver } from '@renderer/stores/driver' +import { useSettings } from '@renderer/stores/settings' + +const PADDING = 16 +const MAX_MONITORS = 16 + +export function useDisplayLayout(): { displays: DisplayLayoutInfo[]; refresh: () => void } { + const [displays, setDisplays] = useState([]) + + const refresh = (): void => { + void window.vdd.system + .displays() + .then(setDisplays) + .catch(() => undefined) + } + + useEffect(() => { + let cancelled = false + let attempts = 0 + // A real system always has at least one display - empty means the call + // raced app startup, so retry briefly. + const fetchLayout = (): void => { + window.vdd.system + .displays() + .then((layout) => { + if (cancelled) return + if (layout.length > 0) setDisplays(layout) + else if (attempts++ < 5) window.setTimeout(fetchLayout, 1200) + }) + .catch(() => { + if (!cancelled && attempts++ < 5) window.setTimeout(fetchLayout, 1200) + }) + } + fetchLayout() + const unsubscribe = window.vdd.events.onDisplays(setDisplays) + return () => { + cancelled = true + unsubscribe() + } + }, []) + + return { displays, refresh } +} + +/** + * Unified desktop canvas: physical and virtual monitors rendered together, + * to scale, in their real Windows arrangement. Virtual monitors only appear + * when they actually exist in the layout. The footer integrates the virtual + * display count control and live legend. + */ +export function DisplayCanvas(): React.JSX.Element { + const { displays, refresh } = useDisplayLayout() + const busy = useDriver((s) => s.busy) + const online = useDriver((s) => s.status?.pipeConnected === true) + const applyDisplayCount = useDriver((s) => s.applyDisplayCount) + const count = useSettings((s) => s.draft.monitors.count) + + const wrapRef = useRef(null) + const [width, setWidth] = useState(880) + + useEffect(() => { + const el = wrapRef.current + if (!el) return + const observer = new ResizeObserver((entries) => { + const w = entries[0]?.contentRect.width ?? 880 + setWidth(Math.max(300, Math.floor(w))) + }) + observer.observe(el) + return () => observer.disconnect() + }, []) + + const height = Math.round(Math.min(340, Math.max(220, width * 0.32))) + const virtualCount = displays.filter((d) => d.isVirtual).length + const physicalCount = displays.length - virtualCount + + let monitors: React.JSX.Element | React.JSX.Element[] + if (displays.length === 0) { + monitors =
Reading display topology…
+ } else { + const minX = Math.min(...displays.map((d) => d.bounds.x)) + const minY = Math.min(...displays.map((d) => d.bounds.y)) + const maxX = Math.max(...displays.map((d) => d.bounds.x + d.bounds.width)) + const maxY = Math.max(...displays.map((d) => d.bounds.y + d.bounds.height)) + const scale = Math.min((width - PADDING * 2) / (maxX - minX), (height - PADDING * 2) / (maxY - minY)) + const offsetX = (width - (maxX - minX) * scale) / 2 + const offsetY = (height - (maxY - minY) * scale) / 2 + + monitors = displays.map((d) => { + const pxW = Math.round(d.bounds.width * d.scaleFactor) + const pxH = Math.round(d.bounds.height * d.scaleFactor) + return ( +
+ + {d.primary && } + {d.label} + + + {pxW}×{pxH} + + + {d.frequency ? `${d.frequency} Hz` : ''} + {d.scaleFactor !== 1 ? ` · ${Math.round(d.scaleFactor * 100)}%` : ''} + + {d.isVirtual && VIRTUAL} +
+ ) + }) + } + + return ( +
+
+ {busy && ( +
+
+ + {busy} +
+
+ )} + {monitors} +
+ +
+
+ {physicalCount} physical + 0 ? 'on' : ''}`}>{virtualCount} virtual + {count > 0 && virtualCount === 0 && ( + + {count} configured — {online ? 'applying…' : 'appears when the driver is running'} + + )} +
+
+ + Virtual displays +
+ + {count} + +
+
+
+
+ ) +} + +export function ArrangementMap(): React.JSX.Element { + return ( + + + + ) +} diff --git a/VirtualDriverControl/src/renderer/src/components/CieDiagram.tsx b/VirtualDriverControl/src/renderer/src/components/CieDiagram.tsx new file mode 100644 index 00000000..23eb3109 --- /dev/null +++ b/VirtualDriverControl/src/renderer/src/components/CieDiagram.tsx @@ -0,0 +1,202 @@ +import { useEffect, useMemo, useRef, useState } from 'react' + +/** + * Interactive CIE 1931 xy chromaticity diagram. + * Canvas renders the spectral horseshoe with approximate sRGB colors; + * an SVG overlay draws the gamut triangle with draggable R/G/B/white handles. + */ + +// CIE 1931 2-degree observer spectral locus (wavelength, x, y), 380-700nm. +const SPECTRAL_LOCUS: Array<[number, number, number]> = [ + [380, 0.1741, 0.005], [390, 0.1738, 0.0049], [400, 0.1733, 0.0048], [410, 0.1726, 0.0048], + [420, 0.1714, 0.0051], [430, 0.1689, 0.0069], [440, 0.1644, 0.0109], [450, 0.1566, 0.0177], + [460, 0.144, 0.0297], [465, 0.1355, 0.0399], [470, 0.1241, 0.0578], [475, 0.1096, 0.0868], + [480, 0.0913, 0.1327], [485, 0.0687, 0.2007], [490, 0.0454, 0.295], [495, 0.0235, 0.4127], + [500, 0.0082, 0.5384], [505, 0.0039, 0.6548], [510, 0.0139, 0.7502], [515, 0.0389, 0.812], + [520, 0.0743, 0.8338], [525, 0.1142, 0.8262], [530, 0.1547, 0.8059], [535, 0.1929, 0.7816], + [540, 0.2296, 0.7543], [545, 0.2658, 0.7243], [550, 0.3016, 0.6923], [555, 0.3373, 0.6589], + [560, 0.3731, 0.6245], [565, 0.4087, 0.5896], [570, 0.4441, 0.5547], [575, 0.4788, 0.5202], + [580, 0.5125, 0.4866], [585, 0.5448, 0.4544], [590, 0.5752, 0.4242], [595, 0.6029, 0.3965], + [600, 0.627, 0.3725], [605, 0.6482, 0.3514], [610, 0.6658, 0.334], [620, 0.6915, 0.3083], + [630, 0.7079, 0.292], [640, 0.719, 0.2809], [650, 0.726, 0.274], [660, 0.73, 0.27], + [680, 0.7334, 0.2666], [700, 0.7347, 0.2653] +] + +const X_MAX = 0.8 +const Y_MAX = 0.9 + +export interface CiePoints { + redX: number + redY: number + greenX: number + greenY: number + blueX: number + blueY: number + whiteX: number + whiteY: number +} + +type HandleId = 'red' | 'green' | 'blue' | 'white' + +function pointInPolygon(x: number, y: number, polygon: Array<[number, number]>): boolean { + let inside = false + for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { + const [xi, yi] = polygon[i] + const [xj, yj] = polygon[j] + if (yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) inside = !inside + } + return inside +} + +function xyToRgb(x: number, y: number): [number, number, number] { + if (y <= 0.0001) return [0, 0, 0] + const Y = 1 + const X = (x * Y) / y + const Z = ((1 - x - y) * Y) / y + let r = 3.2406 * X - 1.5372 * Y - 0.4986 * Z + let g = -0.9689 * X + 1.8758 * Y + 0.0415 * Z + let b = 0.0557 * X - 0.204 * Y + 1.057 * Z + r = Math.max(0, r) + g = Math.max(0, g) + b = Math.max(0, b) + const max = Math.max(r, g, b) + if (max > 0) { + r /= max + g /= max + b /= max + } + const encode = (c: number): number => Math.round(255 * Math.pow(c, 1 / 2.2)) + return [encode(r), encode(g), encode(b)] +} + +const HANDLE_META: Record = { + red: { label: 'R', fill: '#ff5d5d' }, + green: { label: 'G', fill: '#4ade80' }, + blue: { label: 'B', fill: '#60a5fa' }, + white: { label: 'W', fill: '#ffffff' } +} + +export function CieDiagram(props: { + value: CiePoints + onChange: (patch: Partial) => void + disabled?: boolean + width?: number +}): React.JSX.Element { + const width = props.width ?? 380 + const height = Math.round((width * Y_MAX) / X_MAX) + const canvasRef = useRef(null) + const svgRef = useRef(null) + const [dragging, setDragging] = useState(null) + + const toPx = useMemo( + () => ({ + x: (cx: number) => (cx / X_MAX) * width, + y: (cy: number) => height - (cy / Y_MAX) * height + }), + [width, height] + ) + + // Render the horseshoe once per size. + useEffect(() => { + const canvas = canvasRef.current + if (!canvas) return + const ctx = canvas.getContext('2d') + if (!ctx) return + + const polygon: Array<[number, number]> = SPECTRAL_LOCUS.map(([, x, y]) => [x, y]) + const image = ctx.createImageData(width, height) + for (let py = 0; py < height; py++) { + for (let px = 0; px < width; px++) { + const cx = (px / width) * X_MAX + const cy = ((height - py) / height) * Y_MAX + if (!pointInPolygon(cx, cy, polygon)) continue + const [r, g, b] = xyToRgb(cx, cy) + const idx = (py * width + px) * 4 + image.data[idx] = r + image.data[idx + 1] = g + image.data[idx + 2] = b + image.data[idx + 3] = 235 + } + } + ctx.putImageData(image, 0, 0) + }, [width, height]) + + // Pointer dragging on the SVG overlay. + useEffect(() => { + if (!dragging) return + const svg = svgRef.current + if (!svg) return + + const onMove = (event: PointerEvent): void => { + const rect = svg.getBoundingClientRect() + const cx = Math.max(0.001, Math.min(X_MAX, ((event.clientX - rect.left) / rect.width) * X_MAX)) + const cy = Math.max(0.001, Math.min(Y_MAX, ((rect.bottom - event.clientY) / rect.height) * Y_MAX)) + const rx = Math.round(cx * 10000) / 10000 + const ry = Math.round(cy * 10000) / 10000 + if (dragging === 'red') props.onChange({ redX: rx, redY: ry }) + else if (dragging === 'green') props.onChange({ greenX: rx, greenY: ry }) + else if (dragging === 'blue') props.onChange({ blueX: rx, blueY: ry }) + else props.onChange({ whiteX: rx, whiteY: ry }) + } + const onUp = (): void => setDragging(null) + + window.addEventListener('pointermove', onMove) + window.addEventListener('pointerup', onUp) + return () => { + window.removeEventListener('pointermove', onMove) + window.removeEventListener('pointerup', onUp) + } + }, [dragging, props]) + + const v = props.value + const handles: Array<{ id: HandleId; x: number; y: number }> = [ + { id: 'red', x: v.redX, y: v.redY }, + { id: 'green', x: v.greenX, y: v.greenY }, + { id: 'blue', x: v.blueX, y: v.blueY }, + { id: 'white', x: v.whiteX, y: v.whiteY } + ] + + const trianglePoints = `${toPx.x(v.redX)},${toPx.y(v.redY)} ${toPx.x(v.greenX)},${toPx.y(v.greenY)} ${toPx.x(v.blueX)},${toPx.y(v.blueY)}` + // sRGB reference triangle for comparison. + const srgbPoints = `${toPx.x(0.64)},${toPx.y(0.33)} ${toPx.x(0.3)},${toPx.y(0.6)} ${toPx.x(0.15)},${toPx.y(0.06)}` + + return ( +
+ + + + + {handles.map((h) => ( + { + if (props.disabled) return + e.preventDefault() + setDragging(h.id) + }} + > + + + + {HANDLE_META[h.id].label} + + + ))} + +
+ ) +} diff --git a/VirtualDriverControl/src/renderer/src/components/DriverLifecycle.tsx b/VirtualDriverControl/src/renderer/src/components/DriverLifecycle.tsx new file mode 100644 index 00000000..f41bd389 --- /dev/null +++ b/VirtualDriverControl/src/renderer/src/components/DriverLifecycle.tsx @@ -0,0 +1,250 @@ +import { useEffect, useState } from 'react' +import { + ArrowUpCircle, + Download, + ExternalLink, + Layers, + Loader2, + PackageCheck, + PackageOpen, + Power, + RefreshCw, + ShieldAlert, + ShieldCheck, + Trash2 +} from 'lucide-react' +import type { ManagedDriverId } from '@shared/types' +import { Card, Segmented } from '@renderer/components/ui' +import { useDriver } from '@renderer/stores/driver' +import { useInstaller } from '@renderer/stores/installer' + +const TITLES: Record = { + display: 'Display driver lifecycle', + audio: 'Audio driver lifecycle' +} + +const REPOS: Record = { + display: 'VirtualDrivers/Virtual-Display-Driver', + audio: 'VirtualDrivers/Virtual-Audio-Driver' +} + +function formatSize(bytes: number): string { + if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB` + return `${Math.max(1, Math.round(bytes / 1024))} KB` +} + +function formatDate(iso: string): string { + const date = new Date(iso) + return Number.isNaN(date.getTime()) ? iso : date.toLocaleDateString() +} + +export function DriverLifecycle(props: { driver: ManagedDriverId }): React.JSX.Element { + const { driver } = props + const multiInstance = driver === 'audio' + + const state = useInstaller((s) => s.drivers[driver]) + const working = useInstaller((s) => s.working) + const progress = useInstaller((s) => s.progress) + const init = useInstaller((s) => s.init) + const checkLatest = useInstaller((s) => s.checkLatest) + const install = useInstaller((s) => s.install) + const uninstallDriver = useInstaller((s) => s.uninstallDriver) + const restartDevice = useInstaller((s) => s.restartDevice) + const setInstances = useInstaller((s) => s.setInstances) + const testSigning = useInstaller((s) => s.testSigning) + const setTestSigning = useInstaller((s) => s.setTestSigning) + const displayStatus = useDriver((s) => s.status) + + const [instanceChoice, setInstanceChoice] = useState(null) + + useEffect(() => { + init() + if (!useInstaller.getState().drivers[driver].latest && !useInstaller.getState().drivers[driver].checking) { + void checkLatest(driver) + } + }, [init, checkLatest, driver]) + + const { latest, installedTag, device, checking, checkError } = state + const installed = (device?.count ?? 0) > 0 + const updateAvailable = installed && latest !== null && installedTag !== null && latest.tag !== installedTag + const busy = working !== null + const busyHere = working?.driver === driver + const deviceCount = device?.count ?? 0 + const selectedInstances = instanceChoice ?? Math.max(deviceCount, 1) + + const installLabel = !installed + ? latest + ? `Download & install ${latest.tag}` + : 'Download & install latest' + : updateAvailable + ? `Update to ${latest?.tag}` + : 'Reinstall latest' + + const installedDetail = (): string => { + if (!installed) return 'No device on this system' + const statuses = device?.statuses.join(', ') ?? '' + if (driver === 'display') { + return displayStatus?.dllDate ? `MttVDD.dll · ${displayStatus.dllDate}` : `device status ${statuses}` + } + return `${deviceCount} device${deviceCount === 1 ? '' : 's'} · status ${statuses}` + } + + return ( + void checkLatest(driver)}> + {checking ? : } Check for updates + + } + > +
+
+ + Installed + + + {installed ? installedTag ?? 'Installed' : 'Not installed'} + + {installedDetail()} +
+ +
+ + Latest release + + {checking ? 'Checking…' : latest?.tag ?? '—'} + + {checkError + ? `Check failed: ${checkError}` + : latest + ? `${formatDate(latest.publishedAt)}${latest.asset ? ` · ${latest.asset.name} (${formatSize(latest.asset.sizeBytes)})` : ' · no driver package'}` + : 'Fetching from GitHub…'} + + {latest && ( + + )} +
+
+ + {driver === 'audio' && ( +
+ {testSigning === true ? : } + + The Virtual Audio Driver is currently test-signed, so Windows must run in{' '} + Test Signing mode for the device to start.{' '} + {testSigning === true && 'Test signing is enabled in this PC\u2019s boot configuration.'} + {testSigning === false && 'Test signing is currently OFF on this PC.'} + {testSigning === null && 'The current test signing state could not be determined.'} + {testSigning !== null && ' Changes take effect after a Windows restart. Secure Boot must be disabled to enable it.'} + + {testSigning !== null && ( + + )} +
+ )} + + {busyHere && progress && ( +
+
+
= 0 ? { width: `${progress.percent}%` } : undefined} + /> +
+ {progress.message} +
+ )} + +
+ + {installed && ( + <> + + + + )} + {updateAvailable && ( + + Update available: {installedTag} → {latest?.tag} + + )} +
+ + {multiInstance && ( +
+ + Devices + + ({ value: String(n), label: String(n) }))} + onChange={(v) => setInstanceChoice(Number(v))} + /> + {installed && selectedInstances !== deviceCount && ( + + )} + + Each device adds an independent virtual speaker + microphone pair for routing. + +
+ )} + + ) +} diff --git a/VirtualDriverControl/src/renderer/src/components/SaveBar.tsx b/VirtualDriverControl/src/renderer/src/components/SaveBar.tsx new file mode 100644 index 00000000..e41c1694 --- /dev/null +++ b/VirtualDriverControl/src/renderer/src/components/SaveBar.tsx @@ -0,0 +1,47 @@ +import { AnimatePresence, motion } from 'motion/react' +import { Loader2, RotateCcw, Save, Zap } from 'lucide-react' +import { useDriver } from '@renderer/stores/driver' +import { useSettings } from '@renderer/stores/settings' + +export function SaveBar(): React.JSX.Element { + const dirty = useSettings((s) => s.dirty) + const discard = useSettings((s) => s.discard) + const save = useSettings((s) => s.save) + const busy = useDriver((s) => s.busy) + const online = useDriver((s) => s.status?.pipeConnected === true) + const saveAndApply = useDriver((s) => s.saveAndApply) + + return ( + + {dirty && ( + + Unsaved configuration changes + + + + + )} + + ) +} diff --git a/VirtualDriverControl/src/renderer/src/components/Sidebar.tsx b/VirtualDriverControl/src/renderer/src/components/Sidebar.tsx new file mode 100644 index 00000000..bb4fba1a --- /dev/null +++ b/VirtualDriverControl/src/renderer/src/components/Sidebar.tsx @@ -0,0 +1,66 @@ +import { + Cpu, + LayoutDashboard, + MonitorCog, + Palette, + ScanEye, + SettingsIcon, + SquareTerminal, + Volume2 +} from 'lucide-react' +import type { LucideIcon } from 'lucide-react' +import { useDriver } from '@renderer/stores/driver' +import { useUi, type PageId } from '@renderer/stores/ui' + +interface NavEntry { + id: PageId + label: string + icon: LucideIcon + section?: string +} + +const NAV: NavEntry[] = [ + { id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard }, + { id: 'displays', label: 'Displays', icon: MonitorCog, section: 'Configure' }, + { id: 'color', label: 'HDR & Color', icon: Palette }, + { id: 'edid', label: 'EDID Lab', icon: ScanEye }, + { id: 'gpu', label: 'GPU', icon: Cpu }, + { id: 'audio', label: 'Audio', icon: Volume2 }, + { id: 'console', label: 'Console', icon: SquareTerminal, section: 'Diagnostics' }, + { id: 'settings', label: 'Settings', icon: SettingsIcon } +] + +export function Sidebar(): React.JSX.Element { + const page = useUi((s) => s.page) + const setPage = useUi((s) => s.setPage) + const sysInfo = useDriver((s) => s.sysInfo) + const iddcx = useDriver((s) => s.iddcx) + + return ( + + ) +} diff --git a/VirtualDriverControl/src/renderer/src/components/TitleBar.tsx b/VirtualDriverControl/src/renderer/src/components/TitleBar.tsx new file mode 100644 index 00000000..aa859596 --- /dev/null +++ b/VirtualDriverControl/src/renderer/src/components/TitleBar.tsx @@ -0,0 +1,64 @@ +import { Loader2, Minus, Square, X, Copy, ShieldAlert } from 'lucide-react' +import { useDriver } from '@renderer/stores/driver' +import { useUi } from '@renderer/stores/ui' +import logoOk from '../assets/logo-ok.png' +import logoWarn from '../assets/logo-warn.png' +import logoErr from '../assets/logo-err.png' + +const LEVEL_META: Record = { + online: { className: 'online', label: 'Driver online', logo: logoOk }, + 'installed-offline': { className: 'offline', label: 'Installed, not responding', logo: logoWarn }, + 'not-installed': { className: 'missing', label: 'Driver not installed', logo: logoErr }, + unknown: { className: 'unknown', label: 'Checking driver…', logo: logoErr } +} + +export function TitleBar(): React.JSX.Element { + const status = useDriver((s) => s.status) + const busy = useDriver((s) => s.busy) + const sysInfo = useDriver((s) => s.sysInfo) + const maximized = useUi((s) => s.maximized) + + const meta = LEVEL_META[status?.level ?? 'unknown'] + + return ( +
+
+ + Virtual Driver Control +
+ +
+ + {meta.label} +
+ + {busy && ( +
+ + {busy} +
+ )} + + {sysInfo && !sysInfo.isAdmin && ( +
+ + Not elevated +
+ )} + +
+ +
+ + + +
+
+ ) +} diff --git a/VirtualDriverControl/src/renderer/src/components/Toasts.tsx b/VirtualDriverControl/src/renderer/src/components/Toasts.tsx new file mode 100644 index 00000000..ed5cfa69 --- /dev/null +++ b/VirtualDriverControl/src/renderer/src/components/Toasts.tsx @@ -0,0 +1,42 @@ +import { AnimatePresence, motion } from 'motion/react' +import { AlertTriangle, CheckCircle2, Info, XCircle, X } from 'lucide-react' +import { useUi, type ToastKind } from '@renderer/stores/ui' + +const ICONS: Record = { + success: , + error: , + info: , + warning: +} + +export function Toasts(): React.JSX.Element { + const toasts = useUi((s) => s.toasts) + const dismiss = useUi((s) => s.dismissToast) + + return ( +
+ + {toasts.map((toast) => ( + + {ICONS[toast.kind]} +
+
{toast.title}
+ {toast.message &&
{toast.message}
} +
+ +
+ ))} +
+
+ ) +} diff --git a/VirtualDriverControl/src/renderer/src/components/ui.tsx b/VirtualDriverControl/src/renderer/src/components/ui.tsx new file mode 100644 index 00000000..b3b96a7b --- /dev/null +++ b/VirtualDriverControl/src/renderer/src/components/ui.tsx @@ -0,0 +1,185 @@ +import { AnimatePresence, motion } from 'motion/react' +import type { LucideIcon } from 'lucide-react' +import type { ReactNode } from 'react' + +// --------------------------------------------------------------------------- +// Card +// --------------------------------------------------------------------------- + +export function Card(props: { + icon?: LucideIcon + title?: string + subtitle?: string + actions?: ReactNode + children: ReactNode + className?: string +}): React.JSX.Element { + const Icon = props.icon + return ( +
+ {props.title && ( +
+ {Icon && ( + + + + )} +
+

{props.title}

+ {props.subtitle &&
{props.subtitle}
} +
+ {props.actions &&
{props.actions}
} +
+ )} +
{props.children}
+
+ ) +} + +// --------------------------------------------------------------------------- +// Toggle +// --------------------------------------------------------------------------- + +export function Toggle(props: { + checked: boolean + onChange: (value: boolean) => void + label?: string + sublabel?: string + disabled?: boolean +}): React.JSX.Element { + return ( + + ) +} + +// --------------------------------------------------------------------------- +// Field wrappers +// --------------------------------------------------------------------------- + +export function Field(props: { label: string; hint?: string; children: ReactNode }): React.JSX.Element { + return ( +
+ + {props.children} + {props.hint && {props.hint}} +
+ ) +} + +export function NumberField(props: { + label: string + value: number + onChange: (value: number) => void + min?: number + max?: number + step?: number + hint?: string + disabled?: boolean +}): React.JSX.Element { + return ( + + { + const n = Number(e.target.value) + if (Number.isFinite(n)) props.onChange(n) + }} + /> + + ) +} + +// --------------------------------------------------------------------------- +// Segmented control +// --------------------------------------------------------------------------- + +export function Segmented(props: { + value: T + options: Array<{ value: T; label: string }> + onChange: (value: T) => void +}): React.JSX.Element { + return ( +
+ {props.options.map((opt) => ( + + ))} +
+ ) +} + +// --------------------------------------------------------------------------- +// Modal +// --------------------------------------------------------------------------- + +export function Modal(props: { + open: boolean + title: string + icon?: LucideIcon + onClose: () => void + footer?: ReactNode + children: ReactNode + wide?: boolean +}): React.JSX.Element { + const Icon = props.icon + return ( + + {props.open && ( + { + if (e.target === e.currentTarget) props.onClose() + }} + > + +
+ {Icon && } +

{props.title}

+
+
{props.children}
+ {props.footer &&
{props.footer}
} +
+
+ )} +
+ ) +} diff --git a/VirtualDriverControl/src/renderer/src/env.d.ts b/VirtualDriverControl/src/renderer/src/env.d.ts new file mode 100644 index 00000000..188b5b4f --- /dev/null +++ b/VirtualDriverControl/src/renderer/src/env.d.ts @@ -0,0 +1,9 @@ +declare module '*.css' { + const css: string + export default css +} + +declare module '*.png' { + const src: string + export default src +} diff --git a/VirtualDriverControl/src/renderer/src/main.tsx b/VirtualDriverControl/src/renderer/src/main.tsx new file mode 100644 index 00000000..81c4c6a2 --- /dev/null +++ b/VirtualDriverControl/src/renderer/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import './styles/global.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +) diff --git a/VirtualDriverControl/src/renderer/src/pages/AudioPage.tsx b/VirtualDriverControl/src/renderer/src/pages/AudioPage.tsx new file mode 100644 index 00000000..5d064728 --- /dev/null +++ b/VirtualDriverControl/src/renderer/src/pages/AudioPage.tsx @@ -0,0 +1,284 @@ +import { useEffect, useRef, useState } from 'react' +import { + ArrowRight, + AudioLines, + Check, + Info, + Loader2, + Mic, + MonitorSpeaker, + Plus, + RefreshCw, + Trash2, + Volume2, + VolumeX, + Waves +} from 'lucide-react' +import type { AudioEndpoint } from '@shared/types' +import { Card, Toggle } from '@renderer/components/ui' +import { DriverLifecycle } from '@renderer/components/DriverLifecycle' +import { SYSTEM_AUDIO_SOURCE, useAudio } from '@renderer/stores/audio' +import { audioRouter } from '@renderer/utils/audio-router' + +export function AudioPage(): React.JSX.Element { + const init = useAudio((s) => s.init) + + useEffect(() => { + void init() + }, [init]) + + return ( +
+
+
+

Audio

+

Virtual audio devices, Windows endpoint control and live routing

+
+
+ + + + +
+ ) +} + +// --------------------------------------------------------------------------- +// Windows endpoints +// --------------------------------------------------------------------------- + +function EndpointsCard(): React.JSX.Element { + const endpoints = useAudio((s) => s.endpoints) + const loading = useAudio((s) => s.endpointsLoading) + const error = useAudio((s) => s.endpointsError) + const refresh = useAudio((s) => s.refreshEndpoints) + + const outputs = endpoints.filter((e) => e.flow === 'render') + const inputs = endpoints.filter((e) => e.flow === 'capture') + + return ( + void refresh()}> + {loading ? : } Refresh + + } + > + {error &&
{error}
} + {endpoints.length === 0 && !error && ( +
{loading ? 'Enumerating audio endpoints…' : 'No active audio endpoints found.'}
+ )} +
+
+
+ Output · {outputs.length} +
+
+ {outputs.map((e) => ( + + ))} +
+
+
+
+ Input · {inputs.length} +
+
+ {inputs.map((e) => ( + + ))} +
+
+
+
+ ) +} + +function EndpointRow(props: { endpoint: AudioEndpoint }): React.JSX.Element { + const { endpoint } = props + const setVolume = useAudio((s) => s.setEndpointVolume) + const setMute = useAudio((s) => s.setEndpointMute) + const setDefault = useAudio((s) => s.setDefaultEndpoint) + + return ( +
+ {endpoint.flow === 'render' ? : } +
+
+ {endpoint.name} + {endpoint.isVirtual && virtual} + {endpoint.isDefault && default} + {endpoint.isDefaultComm && comms} +
+
+ + setVolume(endpoint.id, Number(e.target.value) / 100)} + /> + {Math.round(endpoint.volume * 100)}% + {!endpoint.isDefault && ( + + )} +
+
+
+ ) +} + +// --------------------------------------------------------------------------- +// Routing +// --------------------------------------------------------------------------- + +function RoutingCard(): React.JSX.Element { + const routes = useAudio((s) => s.routes) + const webInputs = useAudio((s) => s.webInputs) + const webOutputs = useAudio((s) => s.webOutputs) + const activeRoutes = useAudio((s) => s.activeRoutes) + const routeErrors = useAudio((s) => s.routeErrors) + const addRoute = useAudio((s) => s.addRoute) + const removeRoute = useAudio((s) => s.removeRoute) + const toggleRoute = useAudio((s) => s.toggleRoute) + const setRouteGain = useAudio((s) => s.setRouteGain) + const refreshWebDevices = useAudio((s) => s.refreshWebDevices) + + const [sourceId, setSourceId] = useState('') + const [sinkId, setSinkId] = useState('') + + const sourceOptions = [{ deviceId: SYSTEM_AUDIO_SOURCE, label: 'System audio (default output loopback)' }, ...webInputs] + + const add = (): void => { + const source = sourceOptions.find((d) => d.deviceId === sourceId) + const sink = webOutputs.find((d) => d.deviceId === sinkId) + if (!source || !sink) return + void addRoute(source.deviceId, source.label, sink.deviceId, sink.label) + } + + return ( + void refreshWebDevices()}> + Rescan devices + + } + > +
+ + + + +
+ +
+ {routes.map((route) => ( +
+ void toggleRoute(route.id, v)} /> +
+
+ + {route.sourceId === SYSTEM_AUDIO_SOURCE ? : } + {route.sourceLabel} + + + + + {route.sinkLabel} + +
+
+ + Gain + + setRouteGain(route.id, Number(e.target.value) / 100)} + /> + {Math.round(route.gain * 100)}% + +
+ {routeErrors[route.id] &&
{routeErrors[route.id]}
} +
+ +
+ ))} + {routes.length === 0 &&
No routes yet - add one above.
} +
+ +
+ + + Mic → speaker: pick a microphone and an output. Speaker → speaker: use{' '} + System audio as the source. Speaker → mic: route into{' '} + Speakers (Virtual Audio Driver) - apps then hear it on the matching virtual microphone. + +
+
+ ) +} + +function RouteMeter(props: { routeId: string; active: boolean }): React.JSX.Element { + const barRef = useRef(null) + + useEffect(() => { + if (!props.active) return + let raf = 0 + const tick = (): void => { + if (barRef.current) { + barRef.current.style.width = `${Math.round(audioRouter.level(props.routeId) * 100)}%` + } + raf = requestAnimationFrame(tick) + } + raf = requestAnimationFrame(tick) + return () => cancelAnimationFrame(raf) + }, [props.routeId, props.active]) + + return ( +
+
+
+ ) +} diff --git a/VirtualDriverControl/src/renderer/src/pages/ColorPage.tsx b/VirtualDriverControl/src/renderer/src/pages/ColorPage.tsx new file mode 100644 index 00000000..68cb11e1 --- /dev/null +++ b/VirtualDriverControl/src/renderer/src/pages/ColorPage.tsx @@ -0,0 +1,264 @@ +import { Blend, Palette, Sparkles, SunMedium, Triangle } from 'lucide-react' +import { COLOR_SPACE_PRESETS } from '@shared/defaults' +import type { ColourFormat } from '@shared/types' +import { CieDiagram } from '@renderer/components/CieDiagram' +import { Card, Field, NumberField, Segmented, Toggle } from '@renderer/components/ui' +import { useSettings } from '@renderer/stores/settings' + +const COLOUR_FORMATS: Array<{ value: ColourFormat; label: string; sub: string }> = [ + { value: 'RGB', label: 'RGB', sub: 'Full fidelity, default' }, + { value: 'YCbCr444', label: 'YCbCr 4:4:4', sub: 'No chroma subsampling' }, + { value: 'YCbCr422', label: 'YCbCr 4:2:2', sub: 'Half chroma bandwidth' }, + { value: 'YCbCr420', label: 'YCbCr 4:2:0', sub: 'Quarter chroma, streaming' } +] + +export function ColorPage(): React.JSX.Element { + const draft = useSettings((s) => s.draft) + const patch = useSettings((s) => s.patch) + + const primaries = draft.hdrAdvanced.colorPrimaries + const hdr10 = draft.hdrAdvanced.hdr10StaticMetadata + const colorSpace = draft.hdrAdvanced.colorSpace + + return ( +
+
+
+

HDR & Color

+

Pixel format, HDR10 metadata and the color gamut your virtual displays advertise

+
+
+ + +
+
+ {COLOUR_FORMATS.map((format) => ( + + ))} +
+
+ patch((d) => (d.colour.hdrPlus = v))} + label="HDR+" + sublabel="Requires Windows 11 23H2+" + /> + patch((d) => (d.colour.sdr10bit = v))} + label="SDR 10-bit" + sublabel="10-bit output without HDR" + /> +
+
+
+ + patch((d) => (d.hdrAdvanced.hdr10StaticMetadata.enabled = v))} />} + > +
+ patch((d) => (d.hdrAdvanced.hdr10StaticMetadata.maxDisplayMasteringLuminance = v))} + /> + patch((d) => (d.hdrAdvanced.hdr10StaticMetadata.minDisplayMasteringLuminance = v))} + /> + patch((d) => (d.hdrAdvanced.hdr10StaticMetadata.maxContentLightLevel = v))} + /> + patch((d) => (d.hdrAdvanced.hdr10StaticMetadata.maxFrameAvgLightLevel = v))} + /> +
+
+ + patch((d) => (d.hdrAdvanced.colorPrimaries.enabled = v))} />} + > +
+ + patch((d) => { + Object.assign(d.hdrAdvanced.colorPrimaries, p) + }) + } + /> +
+ +
+ {Object.entries(COLOR_SPACE_PRESETS).map(([name, preset]) => ( + + ))} +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Primaryxy
Red{primaries.redX.toFixed(4)}{primaries.redY.toFixed(4)}
Green{primaries.greenX.toFixed(4)}{primaries.greenY.toFixed(4)}
Blue{primaries.blueX.toFixed(4)}{primaries.blueY.toFixed(4)}
White point{primaries.whiteX.toFixed(4)}{primaries.whiteY.toFixed(4)}
+
+
+
+ +
+ patch((d) => (d.hdrAdvanced.colorSpace.enabled = v))} />} + > +
+ + patch((d) => (d.hdrAdvanced.colorSpace.gammaCorrection = Number(e.target.value)))} + /> + + + + + patch((d) => (d.hdrAdvanced.colorSpace.enableMatrixTransform = v))} + label="Matrix transform" + sublabel="Apply color space conversion matrix" + /> +
+
+ + +
+ patch((d) => (d.colorAdvanced.bitDepthManagement.autoSelectFromColorSpace = v))} + label="Auto bit depth" + sublabel="Derive bit depth from color space" + /> + + patch((d) => (d.colorAdvanced.bitDepthManagement.forceBitDepth = Number(v)))} + /> + + patch((d) => (d.colorAdvanced.bitDepthManagement.fp16SurfaceSupport = v))} + label="FP16 surface support" + sublabel="Keep enabled for compatibility" + /> + patch((d) => (d.colorAdvanced.colorFormatExtended.sdrWhiteLevel = v))} + /> +
+
+
+
+ ) +} diff --git a/VirtualDriverControl/src/renderer/src/pages/ConsolePage.tsx b/VirtualDriverControl/src/renderer/src/pages/ConsolePage.tsx new file mode 100644 index 00000000..fa6b7cca --- /dev/null +++ b/VirtualDriverControl/src/renderer/src/pages/ConsolePage.tsx @@ -0,0 +1,187 @@ +import { useEffect, useMemo, useRef, useState } from 'react' +import { ArrowDownToLine, Eraser, FolderOpen, SendHorizontal, SquareTerminal } from 'lucide-react' +import type { LogSeverity, LogSource } from '@shared/types' +import { useDriver } from '@renderer/stores/driver' +import { useLogs } from '@renderer/stores/logs' +import { useUi } from '@renderer/stores/ui' + +const KNOWN_COMMANDS = [ + 'PING', + 'GETSETTINGS', + 'SETDISPLAYCOUNT 1', + 'GETALLGPUS', + 'GETASSIGNEDGPU', + 'IDDCXVERSION', + 'D3DDEVICEGPU', + 'HDRPLUS true', + 'SDR10 true', + 'LOGGING true', + 'LOG_DEBUG true', + 'CUSTOMEDID true', + 'PREVENTSPOOF true', + 'CEAOVERRIDE true', + 'HARDWARECURSOR true', + 'SETGPU "name"' +] + +const SEVERITIES: LogSeverity[] = ['error', 'warning', 'info', 'debug'] +const SOURCES: LogSource[] = ['pipe', 'file', 'app'] + +function formatTime(timestamp: number): string { + const d = new Date(timestamp) + return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}` +} + +export function ConsolePage(): React.JSX.Element { + const events = useLogs((s) => s.events) + const severityFilter = useLogs((s) => s.severityFilter) + const sourceFilter = useLogs((s) => s.sourceFilter) + const search = useLogs((s) => s.search) + const autoScroll = useLogs((s) => s.autoScroll) + const toggleSeverity = useLogs((s) => s.toggleSeverity) + const toggleSource = useLogs((s) => s.toggleSource) + const setSearch = useLogs((s) => s.setSearch) + const setAutoScroll = useLogs((s) => s.setAutoScroll) + const clear = useLogs((s) => s.clear) + const online = useDriver((s) => s.status?.pipeConnected === true) + const toast = useUi((s) => s.toast) + + const [command, setCommand] = useState('') + const [sending, setSending] = useState(false) + const feedRef = useRef(null) + + const filtered = useMemo(() => { + const needle = search.trim().toLowerCase() + return events.filter( + (e) => + severityFilter.has(e.severity) && + sourceFilter.has(e.source) && + (needle.length === 0 || e.message.toLowerCase().includes(needle)) + ) + }, [events, severityFilter, sourceFilter, search]) + + useEffect(() => { + if (autoScroll && feedRef.current) { + feedRef.current.scrollTop = feedRef.current.scrollHeight + } + }, [filtered, autoScroll]) + + const sendCommand = async (): Promise => { + const cmd = command.trim() + if (cmd.length === 0 || sending) return + if (cmd.toUpperCase() === 'RELOAD_DRIVER') { + toast('warning', 'RELOAD_DRIVER is blocked', 'It causes undefined behavior in the driver (upstream issue #351). Use SETDISPLAYCOUNT N instead.') + return + } + setSending(true) + try { + const result = await window.vdd.pipe.sendRaw(cmd) + if (!result.ok) toast('error', 'Command failed', result.error) + setCommand('') + } catch (error) { + toast('error', 'Command rejected', error instanceof Error ? error.message : String(error)) + } finally { + setSending(false) + } + } + + return ( +
+
+
+

Console

+

Unified driver activity - file logs, pipe traffic and app events

+
+
+ + +
+
+ +
+ {SEVERITIES.map((sev) => ( + + ))} + + {SOURCES.map((src) => ( + + ))} + setSearch(e.target.value)} + /> + +
+ +
+ {filtered.length === 0 && ( +
+ +
No log activity yet{online ? ' - try sending PING below' : ''}
+
+ )} + {filtered.map((event) => ( +
+ {formatTime(event.timestamp)} + {event.source} + {event.message} +
+ ))} +
+ +
+ setCommand(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && void sendCommand()} + /> + + {KNOWN_COMMANDS.map((cmd) => ( + + +
+
+ ) +} diff --git a/VirtualDriverControl/src/renderer/src/pages/DashboardPage.tsx b/VirtualDriverControl/src/renderer/src/pages/DashboardPage.tsx new file mode 100644 index 00000000..ef932e33 --- /dev/null +++ b/VirtualDriverControl/src/renderer/src/pages/DashboardPage.tsx @@ -0,0 +1,184 @@ +import { + Activity, + Bug, + Cpu, + FileText, + HardDrive, + Layers, + MonitorPlay, + MousePointer2, + ScanEye, + ShieldAlert, + Sparkles, + Sun +} from 'lucide-react' +import type { LucideIcon } from 'lucide-react' +import type { PipeToggleCommand } from '@shared/types' +import { Card, Toggle } from '@renderer/components/ui' +import { DisplayCanvas, useDisplayLayout } from '@renderer/components/ArrangementMap' +import { DriverLifecycle } from '@renderer/components/DriverLifecycle' +import { useDriver } from '@renderer/stores/driver' +import { useSettings } from '@renderer/stores/settings' +import { useUi } from '@renderer/stores/ui' + +interface QuickToggleDef { + command: PipeToggleCommand + label: string + sub: string + icon: LucideIcon + get: (s: ReturnType['draft']) => boolean +} + +const QUICK_TOGGLES: QuickToggleDef[] = [ + { command: 'HDRPLUS', label: 'HDR+', sub: 'High dynamic range output', icon: Sparkles, get: (s) => s.colour.hdrPlus }, + { command: 'SDR10', label: 'SDR 10-bit', sub: '10-bit color in SDR mode', icon: Sun, get: (s) => s.colour.sdr10bit }, + { + command: 'HARDWARECURSOR', + label: 'Hardware cursor', + sub: 'GPU-composited cursor', + icon: MousePointer2, + get: (s) => s.cursor.hardwareCursor + }, + { command: 'CUSTOMEDID', label: 'Custom EDID', sub: 'Use user_edid.bin identity', icon: ScanEye, get: (s) => s.edid.customEdid }, + { command: 'LOGGING', label: 'File logging', sub: 'Write driver log files', icon: FileText, get: (s) => s.logging.logging }, + { command: 'LOG_DEBUG', label: 'Debug logging', sub: 'Verbose troubleshooting logs', icon: Bug, get: (s) => s.logging.debugLogging } +] + +const LEVEL_LABEL: Record = { + online: 'Online', + 'installed-offline': 'Not responding', + 'not-installed': 'Not installed', + unknown: 'Checking…' +} + +export function DashboardPage(): React.JSX.Element { + const status = useDriver((s) => s.status) + const iddcx = useDriver((s) => s.iddcx) + const busy = useDriver((s) => s.busy) + const quickToggle = useDriver((s) => s.quickToggle) + const sysInfo = useDriver((s) => s.sysInfo) + const draft = useSettings((s) => s.draft) + const isDefault = useSettings((s) => s.isDefault) + const setPage = useUi((s) => s.setPage) + const { displays } = useDisplayLayout() + + const online = status?.pipeConnected === true + const attachedVirtual = displays.filter((d) => d.isVirtual).length + const attachedPhysical = displays.length - attachedVirtual + + return ( +
+
+
+

Dashboard

+

Live control of your virtual displays

+
+
+ + {status?.level === 'not-installed' && ( +
+ + + The Virtual Display Driver is not installed on this system. You can still edit and stage configuration - use the + driver lifecycle card below to download and install the latest official release. + +
+ )} + + {isDefault && status?.level !== 'not-installed' && ( +
+ + No vdd_settings.xml found - showing defaults. Saving will create the configuration file. +
+ )} + + + + + +
+
+ + Driver + + + {LEVEL_LABEL[status?.level ?? 'unknown']} + + {status?.deviceName ?? 'Root\\MttVDD'} +
+
+ + Active displays + + {displays.length > 0 ? displays.length : '—'} + + {displays.length > 0 ? `${attachedPhysical} physical · ${attachedVirtual} virtual · ` : ''} + {draft.monitors.count} configured + +
+
+ + GPU + + + {draft.gpu.friendlyName === 'default' ? 'System default' : draft.gpu.friendlyName} + + + + +
+
+ + IddCx + + {iddcx ?? '—'} + {status?.dllPresent ? `MttVDD.dll · ${status.dllDate ?? ''}` : 'driver DLL not found'} +
+ {sysInfo && ( +
+ + Session + + {sysInfo.isAdmin ? 'Administrator' : 'Standard user'} + {sysInfo.windowsVersion} +
+ )} +
+ + + + +
+ {QUICK_TOGGLES.map((def) => { + const Icon = def.icon + const value = def.get(draft) + return ( +
+ + + +
+
{def.label}
+
{def.sub}
+
+ void quickToggle(def.command, v, def.label)} /> +
+ ) + })} +
+
+
+ ) +} diff --git a/VirtualDriverControl/src/renderer/src/pages/DisplaysPage.tsx b/VirtualDriverControl/src/renderer/src/pages/DisplaysPage.tsx new file mode 100644 index 00000000..fe581c37 --- /dev/null +++ b/VirtualDriverControl/src/renderer/src/pages/DisplaysPage.tsx @@ -0,0 +1,336 @@ +import { useMemo, useState } from 'react' +import { Gauge, LayoutGrid, MonitorCog, Plus, Proportions, Star, Trash2, X } from 'lucide-react' +import { RESOLUTION_PRESETS, REFRESH_RATE_PRESETS, aspectRatioLabel } from '@shared/presets' +import { ArrangementMap } from '@renderer/components/ArrangementMap' +import { Card, Field, Modal, NumberField, Toggle } from '@renderer/components/ui' +import { useSettings } from '@renderer/stores/settings' +import { useUi } from '@renderer/stores/ui' + +const STAGE_COLORS = ['#36c98e', '#58a6ff', '#f0b34c', '#e96bb0', '#9b7bff', '#4dd4d4', '#f0566a', '#a3d65c'] + +function formatRate(rate: number): string { + return Number.isInteger(rate) ? String(rate) : rate.toFixed(2).replace(/0$/, '') +} + +export function DisplaysPage(): React.JSX.Element { + const draft = useSettings((s) => s.draft) + const patch = useSettings((s) => s.patch) + const toast = useUi((s) => s.toast) + + const [newGlobalRate, setNewGlobalRate] = useState('') + const [addOpen, setAddOpen] = useState(false) + const [customW, setCustomW] = useState(1920) + const [customH, setCustomH] = useState(1080) + + const existing = useMemo(() => new Set(draft.resolutions.map((r) => `${r.width}x${r.height}`)), [draft.resolutions]) + + const addResolution = (width: number, height: number): void => { + if (width < 320 || height < 240 || width > 10240 || height > 4320) { + toast('warning', 'Resolution out of range', 'Supported range is 320×240 to 10240×4320.') + return + } + if (existing.has(`${width}x${height}`)) { + toast('info', 'Already configured', `${width}×${height} is already in the list.`) + return + } + patch((d) => { + d.resolutions.push({ width, height, refreshRates: [60] }) + d.resolutions.sort((a, b) => b.width * b.height - a.width * a.height) + }) + } + + const addGlobalRate = (): void => { + const rate = Number(newGlobalRate) + if (!Number.isFinite(rate) || rate < 1 || rate > 1000) return + if (draft.global.refreshRates.includes(rate)) return + patch((d) => { + d.global.refreshRates.push(rate) + d.global.refreshRates.sort((a, b) => a - b) + }) + setNewGlobalRate('') + } + + const sizeComparison = useMemo(() => { + const maxW = Math.max(...draft.resolutions.map((r) => r.width), 1) + const maxH = Math.max(...draft.resolutions.map((r) => r.height), 1) + const scale = Math.min(420 / maxW, 190 / maxH) + return draft.resolutions.map((r, i) => ({ + ...r, + px: Math.round(r.width * scale), + py: Math.round(r.height * scale), + color: STAGE_COLORS[i % STAGE_COLORS.length] + })) + }, [draft.resolutions]) + + return ( +
+
+
+

Displays

+

Resolutions and refresh rates offered by every virtual monitor

+
+
+ + + + +
+ {draft.global.refreshRates.map((rate) => ( + + {formatRate(rate)} Hz + + + ))} + {draft.global.refreshRates.length === 0 && No global rates - each resolution uses only its own.} +
+
+ {REFRESH_RATE_PRESETS.filter((r) => !draft.global.refreshRates.includes(r)).map((rate) => ( + + ))} +
+ setNewGlobalRate(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && addGlobalRate()} + /> + +
+
+
+ + setAddOpen(true)}> + Add resolution + + } + > +
+ {draft.resolutions.map((res, index) => ( + + ))} + {draft.resolutions.length === 0 && ( +
+ + At least one resolution is required for the driver to expose display modes. +
+ )} +
+
+ +
+ +
+ patch((d) => (d.autoResolutions.preferredMode.useEdidPreferred = v))} + label="Use EDID preferred mode" + sublabel="Requires EDID integration with a monitor profile" + /> +
+ patch((d) => (d.autoResolutions.preferredMode.fallbackWidth = v))} + /> + patch((d) => (d.autoResolutions.preferredMode.fallbackHeight = v))} + /> + patch((d) => (d.autoResolutions.preferredMode.fallbackRefresh = v))} + /> +
+
+
+ + +
+ {sizeComparison.map((r) => ( +
+ + {r.width}×{r.height} + +
+ ))} +
+
+
+ + setAddOpen(false)} + footer={ + <> + + + } + wide + > +
+ + setCustomW(Number(e.target.value))} /> + + × + + setCustomH(Number(e.target.value))} /> + + +
+ + {(['HD', 'QHD', '4K & Beyond', 'Ultrawide', 'Standard', 'Portable & Tablet'] as const).map((category) => ( +
+
+ {category} +
+
+ {RESOLUTION_PRESETS.filter((p) => p.category === category).map((preset) => ( + + ))} +
+
+ ))} +
+
+ ) +} + +function ResolutionRow(props: { index: number }): React.JSX.Element | null { + const res = useSettings((s) => s.draft.resolutions[props.index]) + const patch = useSettings((s) => s.patch) + const [newRate, setNewRate] = useState('') + + if (!res) return null + + const addRate = (): void => { + const rate = Number(newRate) + if (!Number.isFinite(rate) || rate < 1 || rate > 1000 || res.refreshRates.includes(rate)) return + patch((d) => { + d.resolutions[props.index].refreshRates.push(rate) + d.resolutions[props.index].refreshRates.sort((a, b) => a - b) + }) + setNewRate('') + } + + return ( +
+ + {res.width}×{res.height} + + {aspectRatioLabel(res.width, res.height)} +
+ {res.refreshRates.map((rate) => ( + + {formatRate(rate)} Hz + + + ))} + setNewRate(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && addRate()} + onBlur={() => newRate && addRate()} + /> +
+ +
+ ) +} diff --git a/VirtualDriverControl/src/renderer/src/pages/EdidPage.tsx b/VirtualDriverControl/src/renderer/src/pages/EdidPage.tsx new file mode 100644 index 00000000..91585953 --- /dev/null +++ b/VirtualDriverControl/src/renderer/src/pages/EdidPage.tsx @@ -0,0 +1,336 @@ +import { useRef, useState } from 'react' +import { + BadgeCheck, + BadgeX, + FileDown, + FileUp, + Fingerprint, + FolderOpen, + MonitorCheck, + ScanEye, + ShieldCheck, + Workflow +} from 'lucide-react' +import { generateMonitorProfileXml, parseEdid } from '@shared/edid' +import type { ParsedEdid } from '@shared/types' +import { CieDiagram } from '@renderer/components/CieDiagram' +import { Card, Field, Toggle } from '@renderer/components/ui' +import { useDriver } from '@renderer/stores/driver' +import { useSettings } from '@renderer/stores/settings' +import { useUi } from '@renderer/stores/ui' + +const SOURCE_LABEL: Record = { + detailed: 'Detailed (DTD)', + 'cea-vic': 'CEA-861 VIC', + standard: 'Standard', + established: 'Established' +} + +export function EdidPage(): React.JSX.Element { + const [edid, setEdid] = useState(null) + const [fileName, setFileName] = useState('') + const [bytes, setBytes] = useState(null) + const [dragOver, setDragOver] = useState(false) + const [exporting, setExporting] = useState(false) + const fileInput = useRef(null) + + const draft = useSettings((s) => s.draft) + const patch = useSettings((s) => s.patch) + const busy = useDriver((s) => s.busy) + const quickToggle = useDriver((s) => s.quickToggle) + const toast = useUi((s) => s.toast) + + const loadFile = async (file: File): Promise => { + if (file.size > 4096) { + toast('warning', 'Not an EDID file', 'EDID blobs are at most a few hundred bytes.') + return + } + const buffer = new Uint8Array(await file.arrayBuffer()) + const parsed = parseEdid(buffer) + setBytes(buffer) + setEdid(parsed) + setFileName(file.name) + if (!parsed.valid && parsed.errors.length > 0) { + toast('error', 'EDID parse failed', parsed.errors[0]) + } else { + toast('success', 'EDID decoded', `${parsed.timings.length} display modes found`) + } + } + + const exportProfile = async (): Promise => { + if (!edid || !bytes) return + setExporting(true) + try { + const xml = generateMonitorProfileXml(edid) + const result = await window.vdd.settings.saveMonitorProfile(xml, bytes) + if (result.ok) { + patch((d) => { + d.edidIntegration.enabled = true + d.edidIntegration.autoConfigureFromEdid = true + }) + toast('success', 'Monitor profile exported', 'monitor_profile.xml + user_edid.bin written. EDID integration enabled in the draft - save to persist.') + } else { + toast('error', 'Export failed', result.error) + } + } finally { + setExporting(false) + } + } + + return ( +
+
+
+

EDID Lab

+

Decode real monitor EDIDs and teach your virtual displays to impersonate them

+
+
+ + + { + const file = e.target.files?.[0] + if (file) void loadFile(file) + e.target.value = '' + }} + /> +
fileInput.current?.click()} + onKeyDown={(e) => e.key === 'Enter' && fileInput.current?.click()} + onDragOver={(e) => { + e.preventDefault() + setDragOver(true) + }} + onDragLeave={() => setDragOver(false)} + onDrop={(e) => { + e.preventDefault() + setDragOver(false) + const file = e.dataTransfer.files?.[0] + if (file) void loadFile(file) + }} + > + +
{fileName || 'Drop an EDID binary here'}
+
+ {fileName ? 'Drop another file to replace' : 'or click to browse - 128/256/512 byte blobs supported'} +
+
+
+ + {edid && ( + <> +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + {edid.screenWidthCm && edid.screenHeightCm && ( + + + + + )} + + + + + + + + + +
Monitor name{edid.displayName ?? '—'}
Manufacturer + {edid.manufacturerId} · product {edid.productCode.toString(16).toUpperCase().padStart(4, '0')}h +
Serial{edid.serialString ?? edid.serialNumber}
Manufactured + {edid.manufactureYear} + {edid.manufactureWeek > 0 && edid.manufactureWeek <= 54 ? `, week ${edid.manufactureWeek}` : ''} +
EDID version + {edid.edidVersion} · {edid.extensionCount} extension block{edid.extensionCount === 1 ? '' : 's'} +
Interface + {edid.digital ? `Digital (${edid.videoInterface ?? 'unknown'})` : 'Analog'} + {edid.bitDepth ? ` · ${edid.bitDepth}-bit` : ''} +
Physical size + {edid.screenWidthCm}×{edid.screenHeightCm} cm ( + {(Math.hypot(edid.screenWidthCm, edid.screenHeightCm) / 2.54).toFixed(1)}″) +
Gamma{edid.gamma?.toFixed(2) ?? '—'}
Checksum + {edid.checksumOk ? ( + + valid + + ) : ( + + invalid + + )} +
+
+ {edid.hasCeaExtension && CEA-861} + {edid.ceaBasicAudio && Audio} + {edid.ceaYcbcr444 && YCbCr 4:4:4} + {edid.ceaYcbcr422 && YCbCr 4:2:2} + {edid.hdr?.eotfPq && HDR10 (PQ)} + {edid.hdr?.eotfHlg && HLG} + {edid.hdr?.maxLuminance && {edid.hdr.maxLuminance} nits peak} +
+
+ + + {edid.chromaticity ? ( + undefined} disabled width={320} /> + ) : ( + No chromaticity data + )} + +
+ + void exportProfile()}> + + Export profile + EDID + + } + > +
+ + + + + + + + + + + + {edid.timings.map((t, i) => ( + + + + + + + + ))} + +
ResolutionRefreshSourcePixel clock
+ {t.width}×{t.height} + {t.interlaced ? 'i' : ''} + {t.refreshHz.toFixed(t.refreshHz % 1 === 0 ? 0 : 3)} Hz + {SOURCE_LABEL[t.source]} + {t.vic ? ` ${t.vic}` : ''} + {t.pixelClockMHz ? `${t.pixelClockMHz.toFixed(2)} MHz` : '—'}{t === edid.preferred && preferred}
+
+
+ + )} + +
+ +
+ void quickToggle('CUSTOMEDID', v, 'Custom EDID')} + label="Use custom EDID" + sublabel="Serve user_edid.bin to Windows instead of the built-in identity" + /> + void quickToggle('PREVENTSPOOF', v, 'Prevent spoof')} + label="Prevent manufacturer spoofing" + sublabel="Keep the original manufacturer ID in the served EDID" + /> + void quickToggle('CEAOVERRIDE', v, 'CEA override')} + label="CEA extension override" + sublabel="Replace the CEA-861 block with driver-generated data" + /> +
+
+ + void window.vdd.system.openPath('edid')}> + Open folder + + } + > +
+ patch((d) => (d.edidIntegration.enabled = v))} + label="Enable EDID integration" + /> + patch((d) => (d.edidIntegration.autoConfigureFromEdid = v))} + label="Auto-configure from profile" + sublabel="Resolutions, color and HDR come from the profile" + /> + patch((d) => (d.edidIntegration.overrideManualSettings = v))} + label="Profile overrides manual settings" + /> + patch((d) => (d.edidIntegration.fallbackOnError = v))} + label="Fall back to manual settings on error" + /> + + patch((d) => (d.edidIntegration.edidProfilePath = e.target.value))} + /> + +
+
+
+
+ ) +} diff --git a/VirtualDriverControl/src/renderer/src/pages/GpuPage.tsx b/VirtualDriverControl/src/renderer/src/pages/GpuPage.tsx new file mode 100644 index 00000000..b412395f --- /dev/null +++ b/VirtualDriverControl/src/renderer/src/pages/GpuPage.tsx @@ -0,0 +1,149 @@ +import { useEffect } from 'react' +import { Cpu, Gpu, Loader2, RefreshCw, Zap } from 'lucide-react' +import { Card } from '@renderer/components/ui' +import { useDriver } from '@renderer/stores/driver' +import { useSettings } from '@renderer/stores/settings' +import { useUi } from '@renderer/stores/ui' + +export function GpuPage(): React.JSX.Element { + const gpus = useDriver((s) => s.gpus) + const gpusLoading = useDriver((s) => s.gpusLoading) + const refreshGpus = useDriver((s) => s.refreshGpus) + const assignGpu = useDriver((s) => s.assignGpu) + const busy = useDriver((s) => s.busy) + const online = useDriver((s) => s.status?.pipeConnected === true) + const assignedName = useSettings((s) => s.draft.gpu.friendlyName) + const setPage = useUi((s) => s.setPage) + + useEffect(() => { + if (gpus.length === 0) void refreshGpus() + }, [gpus.length, refreshGpus]) + + const isDefault = assignedName.trim().toLowerCase() === 'default' + + return ( +
+
+
+

GPU

+

Choose which adapter renders your virtual displays

+
+ +
+ + {!online && ( +
+ + Driver offline - adapters listed from Windows (WMI). Assignments are staged into the draft configuration. +
+ )} + + +
+
+ + + +
+
System default
+
+ Let Windows pick the adapter (recommended for single-GPU systems) +
+
+ {isDefault ? ( + assigned + ) : ( + + )} +
+ + {gpus.map((gpu) => { + const assigned = !isDefault && (gpu.assigned || gpu.name.toLowerCase() === assignedName.toLowerCase()) + return ( +
+ + + +
+
{gpu.name}
+
+ {gpu.source === 'pipe' ? 'Reported by driver' : 'Reported by Windows'} + {gpu.driverVersion ? ` · driver ${gpu.driverVersion}` : ''} + {gpu.vramMB ? ` · ${(gpu.vramMB / 1024).toFixed(1)} GB VRAM` : ''} +
+
+ {assigned ? ( + assigned + ) : ( + + )} +
+ ) + })} + + {gpus.length === 0 && !gpusLoading && No adapters found.} +
+
+ + +
+ + + + +
+
+
+ ) +} diff --git a/VirtualDriverControl/src/renderer/src/pages/SettingsPage.tsx b/VirtualDriverControl/src/renderer/src/pages/SettingsPage.tsx new file mode 100644 index 00000000..1a2f9bbd --- /dev/null +++ b/VirtualDriverControl/src/renderer/src/pages/SettingsPage.tsx @@ -0,0 +1,400 @@ +import { useCallback, useEffect, useState } from 'react' +import { + Archive, + FileCode2, + FileText, + FolderOpen, + History, + MousePointer2, + Paintbrush, + RotateCcw, + Wand2 +} from 'lucide-react' +import { DEFAULT_BASE_DIR, type BackupInfo } from '@shared/types' +import { Card, Field, Modal, NumberField, Segmented, Toggle } from '@renderer/components/ui' +import { useSettings } from '@renderer/stores/settings' +import { useUi } from '@renderer/stores/ui' +import { compactDiff, diffLines } from '@renderer/utils/diff' + +const ACCENT_PRESETS = ['#4cc2ff', '#36c98e', '#9b7bff', '#f0a04c', '#e96bb0', '#f0566a', '#4dd4d4'] + +export function SettingsPage(): React.JSX.Element { + const draft = useSettings((s) => s.draft) + const dirty = useSettings((s) => s.dirty) + const patch = useSettings((s) => s.patch) + const load = useSettings((s) => s.load) + const prefs = useUi((s) => s.prefs) + const updatePrefs = useUi((s) => s.updatePrefs) + const toast = useUi((s) => s.toast) + + const [xmlOpen, setXmlOpen] = useState(false) + const [diff, setDiff] = useState | null>(null) + const [rawXml, setRawXml] = useState('') + const [backups, setBackups] = useState([]) + + const refreshBackups = useCallback(async () => { + setBackups(await window.vdd.settings.backups()) + }, []) + + useEffect(() => { + void refreshBackups() + }, [refreshBackups]) + + const openXmlPreview = async (): Promise => { + const [current, preview] = await Promise.all([window.vdd.settings.raw(), window.vdd.settings.preview(draft)]) + setRawXml(preview) + setDiff(dirty || current === null ? compactDiff(diffLines(current ?? '', preview)) : null) + setXmlOpen(true) + } + + const restoreBackup = async (backup: BackupInfo): Promise => { + const result = await window.vdd.settings.restore(backup.fileName) + if (result.ok) { + await load() + await refreshBackups() + toast('success', 'Backup restored', `${backup.fileName} is now the active configuration. Apply to reload the driver.`) + } else { + toast('error', 'Restore failed', result.error) + } + } + + return ( +
+
+
+

Settings

+

Driver behavior, configuration file management and app preferences

+
+
+ +
+ +
+ patch((d) => (d.cursor.hardwareCursor = v))} + label="Hardware cursor" + sublabel="Composite the cursor on the GPU (recommended)" + /> + patch((d) => (d.cursor.alphaCursorSupport = v))} + label="Alpha-blended cursor" + /> +
+ patch((d) => (d.cursor.cursorMaxX = v))} + /> + patch((d) => (d.cursor.cursorMaxY = v))} + /> +
+ + patch((d) => (d.cursor.xorCursorSupportLevel = Number(v)))} + /> + +
+
+ + +
+ patch((d) => (d.logging.sendLogsThroughPipe = v))} + label="Stream logs through pipe" + sublabel="Lets this app capture live responses from the driver" + /> + patch((d) => (d.logging.logging = v))} + label="File logging" + sublabel="Daily log files in the Logs folder" + /> + patch((d) => (d.logging.debugLogging = v))} + label="Debug logging" + sublabel="Warning: verbose - creates large files quickly" + /> +
+
+
+ + patch((d) => (d.autoResolutions.enabled = v))} />} + > +
+ + patch((d) => (d.autoResolutions.sourcePriority = v))} + /> + +
+ patch((d) => (d.autoResolutions.edidModeFiltering.minRefreshRate = v))} + /> + patch((d) => (d.autoResolutions.edidModeFiltering.maxRefreshRate = v))} + /> + patch((d) => (d.autoResolutions.edidModeFiltering.minResolutionWidth = v))} + /> + patch((d) => (d.autoResolutions.edidModeFiltering.maxResolutionWidth = v))} + /> + patch((d) => (d.autoResolutions.edidModeFiltering.minResolutionHeight = v))} + /> + patch((d) => (d.autoResolutions.edidModeFiltering.maxResolutionHeight = v))} + /> +
+ patch((d) => (d.autoResolutions.edidModeFiltering.excludeFractionalRates = v))} + label="Exclude fractional refresh rates" + sublabel="Drop 59.94-style NTSC rates from generated modes" + /> +
+
+ + + + + + } + > +
+
+ + + Backups ({backups.length}) + + + created automatically before every save + +
+
+ {backups.map((backup) => ( +
+ + + {backup.fileName} + + {new Date(backup.createdAt).toLocaleString()} + +
+ ))} + {backups.length === 0 && No backups yet.} +
+
+
+ + +
+ + void updatePrefs({ theme: v })} + /> + + +
+ {ACCENT_PRESETS.map((color) => ( +
+
+ +
+
+ + setXmlOpen(false)} + wide + footer={ + + } + > + {diff ? ( +
+ {diff.map((line, idx) => + 'count' in line ? ( +
+ + {line.count} unchanged lines +
+ ) : ( +
+ {line.type === 'add' ? '+' : line.type === 'del' ? '−' : ''} + {line.text || ' '} +
+ ) + )} +
+ ) : ( +
+            {rawXml}
+          
+ )} +
+
+ ) +} + +/** + * Driver folder control. The app keeps the VDDPATH registry value (which the + * driver reads its settings path from) and this preference in lockstep: + * changes are written to the registry first and only applied once verified. + */ +function DriverFolderField(): React.JSX.Element { + const prefs = useUi((s) => s.prefs) + const setBaseDir = useUi((s) => s.setBaseDir) + const toast = useUi((s) => s.toast) + const load = useSettings((s) => s.load) + + const [value, setValue] = useState(prefs.baseDir) + const [applying, setApplying] = useState(false) + + // Follow external changes (registry sync at startup, reset, etc.). + useEffect(() => setValue(prefs.baseDir), [prefs.baseDir]) + + const isDefault = prefs.baseDir.toLowerCase() === DEFAULT_BASE_DIR.toLowerCase() + const edited = value.trim().length > 0 && value.trim().toLowerCase() !== prefs.baseDir.toLowerCase() + + const apply = async (target: string): Promise => { + setApplying(true) + try { + const result = await setBaseDir(target) + if (result.ok) { + await load() + toast('success', 'Driver folder updated', `Registry and app now point at ${result.prefs.baseDir}. Restart the driver device to apply.`) + } else { + toast('error', 'Folder not changed', result.error) + } + } finally { + setApplying(false) + } + } + + return ( + +
+ setValue(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && edited && void apply(value.trim())} + /> + + {!isDefault && ( + + )} +
+
+ ) +} diff --git a/VirtualDriverControl/src/renderer/src/stores/audio.ts b/VirtualDriverControl/src/renderer/src/stores/audio.ts new file mode 100644 index 00000000..72122071 --- /dev/null +++ b/VirtualDriverControl/src/renderer/src/stores/audio.ts @@ -0,0 +1,220 @@ +import { create } from 'zustand' +import { SYSTEM_AUDIO_SOURCE, type AudioEndpoint, type AudioRoute } from '@shared/types' +import { audioRouter } from '@renderer/utils/audio-router' +import { useUi } from './ui' + +export interface WebAudioDevice { + deviceId: string + label: string +} + +interface AudioState { + endpoints: AudioEndpoint[] + endpointsLoading: boolean + endpointsError: string | null + webInputs: WebAudioDevice[] + webOutputs: WebAudioDevice[] + routes: AudioRoute[] + /** Route ids currently pumping audio. */ + activeRoutes: Set + routeErrors: Record + init: () => Promise + refreshEndpoints: () => Promise + refreshWebDevices: () => Promise + setEndpointVolume: (id: string, volume: number) => void + setEndpointMute: (id: string, muted: boolean) => Promise + setDefaultEndpoint: (id: string) => Promise + addRoute: (sourceId: string, sourceLabel: string, sinkId: string, sinkLabel: string) => Promise + removeRoute: (routeId: string) => Promise + toggleRoute: (routeId: string, enabled: boolean) => Promise + setRouteGain: (routeId: string, gain: number) => void +} + +let initialized = false +const volumeTimers = new Map() + +function persistRoutes(routes: AudioRoute[]): void { + void useUi.getState().updatePrefs({ audioRoutes: routes }) +} + +async function armRoute(route: AudioRoute, set: (fn: (s: AudioState) => Partial) => void): Promise { + try { + await audioRouter.start(route) + set((s) => { + const active = new Set(s.activeRoutes) + active.add(route.id) + const errors = { ...s.routeErrors } + delete errors[route.id] + return { activeRoutes: active, routeErrors: errors } + }) + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + set((s) => ({ routeErrors: { ...s.routeErrors, [route.id]: message } })) + } +} + +export const useAudio = create((set, get) => ({ + endpoints: [], + endpointsLoading: false, + endpointsError: null, + webInputs: [], + webOutputs: [], + routes: [], + activeRoutes: new Set(), + routeErrors: {}, + + init: async () => { + if (initialized) return + initialized = true + + audioRouter.setOnEnded((routeId) => { + set((s) => { + const active = new Set(s.activeRoutes) + active.delete(routeId) + return { + activeRoutes: active, + routeErrors: { ...s.routeErrors, [routeId]: 'Source device stopped (unplugged or removed)' } + } + }) + }) + + navigator.mediaDevices.addEventListener('devicechange', () => void get().refreshWebDevices()) + + const routes = useUi.getState().prefs.audioRoutes ?? [] + set({ routes }) + + void get().refreshEndpoints() + await get().refreshWebDevices() + + // Re-arm persisted routes. + for (const route of routes) { + if (route.enabled) void armRoute(route, set) + } + }, + + refreshEndpoints: async () => { + set({ endpointsLoading: true }) + try { + const endpoints = await window.vdd.audio.endpoints() + set({ endpoints, endpointsError: null }) + } catch (error) { + set({ endpointsError: error instanceof Error ? error.message : String(error) }) + } finally { + set({ endpointsLoading: false }) + } + }, + + refreshWebDevices: async () => { + try { + // A one-shot capture unlocks device labels for enumerateDevices. + if (get().webInputs.every((d) => !d.label)) { + try { + const probe = await navigator.mediaDevices.getUserMedia({ audio: true }) + for (const track of probe.getTracks()) track.stop() + } catch { + // no mic permission/device - labels may stay generic + } + } + const devices = await navigator.mediaDevices.enumerateDevices() + const dedupe = (kind: MediaDeviceKind): WebAudioDevice[] => + devices + .filter((d) => d.kind === kind && d.deviceId !== 'default' && d.deviceId !== 'communications') + .map((d) => ({ deviceId: d.deviceId, label: d.label || 'Unnamed device' })) + set({ webInputs: dedupe('audioinput'), webOutputs: dedupe('audiooutput') }) + } catch { + // media enumeration unavailable + } + }, + + setEndpointVolume: (id, volume) => { + set((s) => ({ endpoints: s.endpoints.map((e) => (e.id === id ? { ...e, volume } : e)) })) + const existing = volumeTimers.get(id) + if (existing !== undefined) window.clearTimeout(existing) + volumeTimers.set( + id, + window.setTimeout(() => { + volumeTimers.delete(id) + window.vdd.audio.setVolume(id, volume).catch((error: unknown) => { + useUi.getState().toast('error', 'Volume change failed', error instanceof Error ? error.message : String(error)) + }) + }, 150) + ) + }, + + setEndpointMute: async (id, muted) => { + set((s) => ({ endpoints: s.endpoints.map((e) => (e.id === id ? { ...e, muted } : e)) })) + try { + await window.vdd.audio.setMute(id, muted) + } catch (error) { + useUi.getState().toast('error', 'Mute change failed', error instanceof Error ? error.message : String(error)) + await get().refreshEndpoints() + } + }, + + setDefaultEndpoint: async (id) => { + try { + await window.vdd.audio.setDefault(id) + await get().refreshEndpoints() + const endpoint = get().endpoints.find((e) => e.id === id) + useUi.getState().toast('success', 'Default device changed', endpoint?.name) + } catch (error) { + useUi.getState().toast('error', 'Failed to set default device', error instanceof Error ? error.message : String(error)) + } + }, + + addRoute: async (sourceId, sourceLabel, sinkId, sinkLabel) => { + const route: AudioRoute = { + id: `route-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`, + sourceId, + sourceLabel, + sinkId, + sinkLabel, + gain: 1, + enabled: true + } + const routes = [...get().routes, route] + set({ routes }) + persistRoutes(routes) + await armRoute(route, set) + }, + + removeRoute: async (routeId) => { + audioRouter.stop(routeId) + const routes = get().routes.filter((r) => r.id !== routeId) + set((s) => { + const active = new Set(s.activeRoutes) + active.delete(routeId) + const errors = { ...s.routeErrors } + delete errors[routeId] + return { routes, activeRoutes: active, routeErrors: errors } + }) + persistRoutes(routes) + }, + + toggleRoute: async (routeId, enabled) => { + const routes = get().routes.map((r) => (r.id === routeId ? { ...r, enabled } : r)) + set({ routes }) + persistRoutes(routes) + const route = routes.find((r) => r.id === routeId) + if (!route) return + if (enabled) { + await armRoute(route, set) + } else { + audioRouter.stop(routeId) + set((s) => { + const active = new Set(s.activeRoutes) + active.delete(routeId) + return { activeRoutes: active } + }) + } + }, + + setRouteGain: (routeId, gain) => { + const routes = get().routes.map((r) => (r.id === routeId ? { ...r, gain } : r)) + set({ routes }) + audioRouter.setGain(routeId, gain) + persistRoutes(routes) + } +})) + +export { SYSTEM_AUDIO_SOURCE } diff --git a/VirtualDriverControl/src/renderer/src/stores/driver.ts b/VirtualDriverControl/src/renderer/src/stores/driver.ts new file mode 100644 index 00000000..f8300738 --- /dev/null +++ b/VirtualDriverControl/src/renderer/src/stores/driver.ts @@ -0,0 +1,205 @@ +import { create } from 'zustand' +import type { DriverStatus, GpuInfo, PipeToggleCommand, SystemInfo } from '@shared/types' +import { useSettings } from './settings' +import { useUi } from './ui' + +/** Maps pipe toggle commands to their vdd_settings.xml fields (for offline staging). */ +const TOGGLE_PATCHES: Record void> = { + HDRPLUS: (d, v) => (d.colour.hdrPlus = v), + SDR10: (d, v) => (d.colour.sdr10bit = v), + CUSTOMEDID: (d, v) => (d.edid.customEdid = v), + PREVENTSPOOF: (d, v) => (d.edid.preventSpoof = v), + CEAOVERRIDE: (d, v) => (d.edid.edidCeaOverride = v), + HARDWARECURSOR: (d, v) => (d.cursor.hardwareCursor = v), + LOGGING: (d, v) => (d.logging.logging = v), + LOG_DEBUG: (d, v) => (d.logging.debugLogging = v) +} + +interface DriverState { + status: DriverStatus | null + iddcx: string | null + sysInfo: SystemInfo | null + gpus: GpuInfo[] + gpusLoading: boolean + /** Set while a reload-triggering pipe operation is in flight. */ + busy: string | null + init: () => Promise + refreshStatus: (force?: boolean) => Promise + refreshGpus: () => Promise + /** Apply a new virtual display count via SETDISPLAYCOUNT (or stage offline). */ + applyDisplayCount: (count: number) => Promise + /** Toggle a driver feature live via the pipe (or stage offline). */ + quickToggle: (name: PipeToggleCommand, value: boolean, label: string) => Promise + assignGpu: (name: string) => Promise + /** Save the draft config and trigger a driver reload to pick it up. */ + saveAndApply: () => Promise +} + +let initialized = false + +export const useDriver = create((set, get) => ({ + status: null, + iddcx: null, + sysInfo: null, + gpus: [], + gpusLoading: false, + busy: null, + + init: async () => { + if (initialized) return + initialized = true + window.vdd.events.onStatus((status) => { + const previous = get().status + set({ status }) + if (previous && previous.level !== status.level) { + const toast = useUi.getState().toast + if (status.level === 'online') { + toast('success', 'Driver online', 'The virtual display driver is responding.') + // The live pipe may report a more precise IddCx version than the + // offline build-table fallback - refresh now that it answers. + void window.vdd.driver + .iddcxVersion() + .then((iddcx) => set({ iddcx })) + .catch(() => undefined) + } else if (previous.level === 'online') { + toast('warning', 'Driver went offline') + } + } + }) + void get().refreshStatus() + try { + const sysInfo = await window.vdd.system.info() + set({ sysInfo }) + } catch { + // ignore + } + void window.vdd.driver + .iddcxVersion() + .then((iddcx) => set({ iddcx })) + .catch(() => undefined) + }, + + refreshStatus: async (force = false) => { + try { + const status = await window.vdd.driver.status(force) + set({ status }) + } catch { + // main not ready yet + } + }, + + refreshGpus: async () => { + set({ gpusLoading: true }) + try { + const gpus = await window.vdd.driver.gpus() + set({ gpus }) + } finally { + set({ gpusLoading: false }) + } + }, + + applyDisplayCount: async (count) => { + const { status } = get() + const settings = useSettings.getState() + const toast = useUi.getState().toast + + if (!status?.pipeConnected) { + settings.patch((d) => (d.monitors.count = count)) + toast('info', 'Driver offline', 'Display count staged - it will apply when the config is saved and the driver restarts.') + return + } + + set({ busy: count === 0 ? 'Removing all virtual displays…' : `Reconfiguring to ${count} display${count === 1 ? '' : 's'}…` }) + try { + const result = await window.vdd.pipe.setDisplayCount(count) + if (result.ok) { + toast('success', `Display count set to ${count}`, `Driver reloaded in ${(result.durationMs / 1000).toFixed(1)}s`) + } else { + toast('error', 'Failed to set display count', result.error) + } + } finally { + set({ busy: null }) + // The driver rewrote vdd_settings.xml - resync our copy and status. + await useSettings.getState().load() + await get().refreshStatus() + } + }, + + quickToggle: async (name, value, label) => { + const { status } = get() + const settings = useSettings.getState() + const toast = useUi.getState().toast + + if (!status?.pipeConnected) { + settings.patch((d) => TOGGLE_PATCHES[name](d, value)) + toast('info', `${label} staged`, 'Driver offline - save the configuration to persist this change.') + return + } + + const reloads = name !== 'LOGGING' && name !== 'LOG_DEBUG' + if (reloads) set({ busy: `Applying ${label}…` }) + try { + const result = await window.vdd.pipe.toggle(name, value) + if (result.ok) { + toast('success', `${label} ${value ? 'enabled' : 'disabled'}`, reloads ? 'Driver reloaded.' : undefined) + } else { + toast('error', `Failed to toggle ${label}`, result.error) + } + } finally { + if (reloads) set({ busy: null }) + await useSettings.getState().load() + } + }, + + assignGpu: async (name) => { + const { status } = get() + const settings = useSettings.getState() + const toast = useUi.getState().toast + + if (!status?.pipeConnected) { + settings.patch((d) => (d.gpu.friendlyName = name)) + toast('info', 'GPU staged', 'Driver offline - save the configuration to persist this change.') + return + } + + set({ busy: `Assigning ${name}…` }) + try { + const result = await window.vdd.pipe.setGpu(name) + if (result.ok) toast('success', 'GPU assigned', `Virtual displays now render on ${name}.`) + else toast('error', 'Failed to assign GPU', result.error) + } finally { + set({ busy: null }) + await useSettings.getState().load() + await get().refreshGpus() + } + }, + + saveAndApply: async () => { + const settings = useSettings.getState() + const toast = useUi.getState().toast + const count = settings.draft.monitors.count + + const saved = await settings.save() + if (!saved) return + + const { status } = get() + if (!status?.pipeConnected) { + toast('info', 'Saved', 'Driver offline - the new configuration loads on next driver start.') + return + } + + set({ busy: 'Reloading driver with new configuration…' }) + try { + const result = await window.vdd.pipe.setDisplayCount(count) + if (result.ok) { + toast('success', 'Configuration applied', `Driver reloaded in ${(result.durationMs / 1000).toFixed(1)}s`) + } else { + toast('error', 'Reload failed', result.error) + } + } finally { + set({ busy: null }) + await useSettings.getState().load() + await get().refreshStatus() + } + } +})) diff --git a/VirtualDriverControl/src/renderer/src/stores/installer.ts b/VirtualDriverControl/src/renderer/src/stores/installer.ts new file mode 100644 index 00000000..d69b0794 --- /dev/null +++ b/VirtualDriverControl/src/renderer/src/stores/installer.ts @@ -0,0 +1,194 @@ +import { create } from 'zustand' +import type { InstallProgress, LifecycleResult, ManagedDeviceState, ManagedDriverId, ReleaseInfo } from '@shared/types' +import { useDriver } from './driver' +import { useSettings } from './settings' +import { useUi } from './ui' + +export type LifecycleOp = 'install' | 'uninstall' | 'restart' | 'instances' | 'testsigning' + +interface DriverLifecycleState { + latest: ReleaseInfo | null + installedTag: string | null + device: ManagedDeviceState | null + checking: boolean + checkError: string | null +} + +interface InstallerState { + drivers: Record + /** Which driver+operation is in flight (lifecycle ops are globally exclusive). */ + working: { driver: ManagedDriverId; op: LifecycleOp } | null + progress: InstallProgress | null + /** Windows boot-config test signing state (audio driver is test-signed). */ + testSigning: boolean | null + init: () => void + checkLatest: (driver: ManagedDriverId) => Promise + refreshDevice: (driver: ManagedDriverId) => Promise + install: (driver: ManagedDriverId, instances?: number) => Promise + uninstallDriver: (driver: ManagedDriverId) => Promise + restartDevice: (driver: ManagedDriverId) => Promise + setInstances: (driver: ManagedDriverId, count: number) => Promise + refreshTestSigning: () => Promise + setTestSigning: (enabled: boolean) => Promise +} + +const emptyState = (): DriverLifecycleState => ({ + latest: null, + installedTag: null, + device: null, + checking: false, + checkError: null +}) + +let initialized = false + +export const useInstaller = create((set, get) => { + const patchDriver = (driver: ManagedDriverId, patch: Partial): void => { + set((s) => ({ drivers: { ...s.drivers, [driver]: { ...s.drivers[driver], ...patch } } })) + } + + const refreshAfter = async (driver: ManagedDriverId): Promise => { + await get().refreshDevice(driver) + const installedTag = await window.vdd.installer.installedTag(driver).catch(() => null) + patchDriver(driver, { installedTag }) + if (driver === 'display') { + await useDriver.getState().refreshStatus(true) + await useSettings.getState().load() + } else { + // Audio endpoints appear/disappear with the device nodes. + const { useAudio } = await import('./audio') + void useAudio.getState().refreshEndpoints() + void useAudio.getState().refreshWebDevices() + } + } + + const runOp = async ( + driver: ManagedDriverId, + op: LifecycleOp, + action: () => Promise, + successTitle: string, + successMessage: string | undefined, + failTitle: string + ): Promise => { + if (get().working) return + const toast = useUi.getState().toast + set({ working: { driver, op }, progress: null }) + try { + const result = await action() + if (result.ok) toast('success', successTitle, successMessage) + else toast('error', failTitle, result.error ?? result.detail) + } finally { + set({ working: null, progress: null }) + await refreshAfter(driver) + } + } + + return { + drivers: { display: emptyState(), audio: emptyState() }, + working: null, + progress: null, + testSigning: null, + + init: () => { + if (initialized) return + initialized = true + window.vdd.events.onInstallProgress((progress) => set({ progress })) + for (const driver of ['display', 'audio'] as ManagedDriverId[]) { + void window.vdd.installer + .installedTag(driver) + .then((installedTag) => patchDriver(driver, { installedTag })) + .catch(() => undefined) + void get().refreshDevice(driver) + } + void get().refreshTestSigning() + }, + + checkLatest: async (driver) => { + patchDriver(driver, { checking: true, checkError: null }) + try { + const latest = await window.vdd.installer.latestRelease(driver) + patchDriver(driver, { latest }) + } catch (error) { + patchDriver(driver, { checkError: error instanceof Error ? error.message : String(error) }) + } finally { + patchDriver(driver, { checking: false }) + } + }, + + refreshDevice: async (driver) => { + try { + const device = await window.vdd.installer.deviceState(driver) + patchDriver(driver, { device }) + } catch { + // main not ready + } + }, + + install: (driver, instances) => + runOp( + driver, + 'install', + () => window.vdd.installer.install(driver, instances), + 'Driver installed', + driver === 'display' + ? 'The Virtual Display Driver is now installed and starting up.' + : 'The Virtual Audio Driver is now installed - new audio devices should appear shortly.', + 'Install failed' + ), + + uninstallDriver: (driver) => + runOp( + driver, + 'uninstall', + () => window.vdd.installer.uninstall(driver), + 'Driver uninstalled', + driver === 'display' ? 'Configuration files were kept for a future reinstall.' : undefined, + 'Uninstall failed' + ), + + restartDevice: (driver) => + runOp( + driver, + 'restart', + () => window.vdd.installer.restartDevice(driver), + 'Device restarted', + 'The device was disabled and re-enabled.', + 'Restart failed' + ), + + setInstances: (driver, count) => + runOp( + driver, + 'instances', + () => window.vdd.installer.setInstances(driver, count), + 'Device count updated', + `${count} virtual audio device${count === 1 ? '' : 's'} now present.`, + 'Failed to change device count' + ), + + refreshTestSigning: async () => { + try { + set({ testSigning: await window.vdd.installer.testSigning() }) + } catch { + // main not ready + } + }, + + setTestSigning: async (enabled) => { + if (get().working) return + const toast = useUi.getState().toast + set({ working: { driver: 'audio', op: 'testsigning' }, progress: null }) + try { + const result = await window.vdd.installer.setTestSigning(enabled) + if (result.ok) { + toast('success', `Test signing ${enabled ? 'enabled' : 'disabled'}`, 'Restart Windows for the change to take effect.') + } else { + toast('error', 'Test signing change failed', result.error ?? result.detail) + } + } finally { + set({ working: null, progress: null }) + await get().refreshTestSigning() + } + } + } +}) diff --git a/VirtualDriverControl/src/renderer/src/stores/logs.ts b/VirtualDriverControl/src/renderer/src/stores/logs.ts new file mode 100644 index 00000000..75eed125 --- /dev/null +++ b/VirtualDriverControl/src/renderer/src/stores/logs.ts @@ -0,0 +1,66 @@ +import { create } from 'zustand' +import type { LogEvent, LogSeverity, LogSource } from '@shared/types' + +const MAX_EVENTS = 3000 + +interface LogsState { + events: LogEvent[] + severityFilter: Set + sourceFilter: Set + search: string + autoScroll: boolean + init: () => Promise + clear: () => void + setSearch: (value: string) => void + toggleSeverity: (severity: LogSeverity) => void + toggleSource: (source: LogSource) => void + setAutoScroll: (value: boolean) => void +} + +let initialized = false + +export const useLogs = create((set) => ({ + events: [], + severityFilter: new Set(['debug', 'info', 'warning', 'error']), + sourceFilter: new Set(['file', 'pipe', 'app']), + search: '', + autoScroll: true, + + init: async () => { + if (initialized) return + initialized = true + try { + const recent = await window.vdd.logs.recent() + set({ events: recent.slice(-MAX_EVENTS) }) + } catch { + // main not ready - events will arrive via push + } + window.vdd.events.onLogs((incoming) => { + set((s) => { + const merged = [...s.events, ...incoming] + return { events: merged.length > MAX_EVENTS ? merged.slice(merged.length - MAX_EVENTS) : merged } + }) + }) + }, + + clear: () => set({ events: [] }), + setSearch: (value) => set({ search: value }), + + toggleSeverity: (severity) => + set((s) => { + const next = new Set(s.severityFilter) + if (next.has(severity)) next.delete(severity) + else next.add(severity) + return { severityFilter: next } + }), + + toggleSource: (source) => + set((s) => { + const next = new Set(s.sourceFilter) + if (next.has(source)) next.delete(source) + else next.add(source) + return { sourceFilter: next } + }), + + setAutoScroll: (value) => set({ autoScroll: value }) +})) diff --git a/VirtualDriverControl/src/renderer/src/stores/settings.ts b/VirtualDriverControl/src/renderer/src/stores/settings.ts new file mode 100644 index 00000000..3ab0c95d --- /dev/null +++ b/VirtualDriverControl/src/renderer/src/stores/settings.ts @@ -0,0 +1,84 @@ +import { create } from 'zustand' +import { DEFAULT_VDD_SETTINGS } from '@shared/defaults' +import type { VddSettings } from '@shared/types' +import { useUi } from './ui' + +interface SettingsState { + /** Editable draft shown in the UI. */ + draft: VddSettings + /** Last state loaded from disk. */ + saved: VddSettings + isDefault: boolean + loaded: boolean + dirty: boolean + loadError: string | null + load: () => Promise + patch: (mutate: (draft: VddSettings) => void) => void + discard: () => void + /** Persist the draft to vdd_settings.xml. Returns success. */ + save: () => Promise +} + +function clone(settings: VddSettings): VddSettings { + return structuredClone(settings) +} + +function equal(a: VddSettings, b: VddSettings): boolean { + return JSON.stringify(a) === JSON.stringify(b) +} + +export const useSettings = create((set, get) => ({ + draft: clone(DEFAULT_VDD_SETTINGS), + saved: clone(DEFAULT_VDD_SETTINGS), + isDefault: true, + loaded: false, + dirty: false, + loadError: null, + + load: async () => { + try { + const result = await window.vdd.settings.load() + if (result.ok && result.settings) { + set({ + draft: clone(result.settings), + saved: clone(result.settings), + isDefault: result.isDefault, + loaded: true, + dirty: false, + loadError: null + }) + } else { + set({ loaded: true, loadError: result.error ?? 'Failed to load settings' }) + } + } catch (error) { + set({ loaded: true, loadError: error instanceof Error ? error.message : String(error) }) + } + }, + + patch: (mutate) => { + const next = clone(get().draft) + mutate(next) + set({ draft: next, dirty: !equal(next, get().saved) }) + }, + + discard: () => set({ draft: clone(get().saved), dirty: false }), + + save: async () => { + const { draft } = get() + try { + const result = await window.vdd.settings.save(draft) + if (result.ok) { + set({ saved: clone(draft), dirty: false, isDefault: false }) + useUi + .getState() + .toast('success', 'Configuration saved', result.backupCreated ? `Backup: ${result.backupCreated}` : undefined) + return true + } + useUi.getState().toast('error', 'Save failed', result.error) + return false + } catch (error) { + useUi.getState().toast('error', 'Save failed', error instanceof Error ? error.message : String(error)) + return false + } + } +})) diff --git a/VirtualDriverControl/src/renderer/src/stores/ui.ts b/VirtualDriverControl/src/renderer/src/stores/ui.ts new file mode 100644 index 00000000..ca08a5a7 --- /dev/null +++ b/VirtualDriverControl/src/renderer/src/stores/ui.ts @@ -0,0 +1,86 @@ +import { create } from 'zustand' +import type { AppPreferences, BaseDirResult } from '@shared/types' + +export type PageId = 'dashboard' | 'displays' | 'color' | 'edid' | 'gpu' | 'audio' | 'console' | 'settings' + +export type ToastKind = 'success' | 'error' | 'info' | 'warning' + +export interface Toast { + id: number + kind: ToastKind + title: string + message?: string +} + +interface UiState { + page: PageId + prefs: AppPreferences + prefsLoaded: boolean + toasts: Toast[] + maximized: boolean + setPage: (page: PageId) => void + toast: (kind: ToastKind, title: string, message?: string) => void + dismissToast: (id: number) => void + initPrefs: () => Promise + updatePrefs: (patch: Partial) => Promise + /** Changes the driver folder (syncs the VDDPATH registry value in main). */ + setBaseDir: (baseDir: string) => Promise + setMaximized: (value: boolean) => void +} + +let toastId = 1 + +function applyTheme(prefs: AppPreferences): void { + const root = document.documentElement + const resolved = + prefs.theme === 'system' + ? window.matchMedia('(prefers-color-scheme: light)').matches + ? 'light' + : 'dark' + : prefs.theme + root.dataset.theme = resolved + root.style.setProperty('--accent', prefs.accent) +} + +export const useUi = create((set, get) => ({ + page: 'dashboard', + prefs: { theme: 'dark', accent: '#4cc2ff', baseDir: 'C:\\VirtualDisplayDriver', audioRoutes: [] }, + prefsLoaded: false, + toasts: [], + maximized: false, + + setPage: (page) => set({ page }), + + toast: (kind, title, message) => { + const id = toastId++ + set((s) => ({ toasts: [...s.toasts.slice(-3), { id, kind, title, message }] })) + window.setTimeout(() => get().dismissToast(id), kind === 'error' ? 7000 : 4200) + }, + + dismissToast: (id) => set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) })), + + initPrefs: async () => { + try { + const prefs = await window.vdd.prefs.get() + applyTheme(prefs) + set({ prefs, prefsLoaded: true }) + window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', () => applyTheme(get().prefs)) + } catch { + set({ prefsLoaded: true }) + } + }, + + updatePrefs: async (patch) => { + const prefs = await window.vdd.prefs.set(patch) + applyTheme(prefs) + set({ prefs }) + }, + + setBaseDir: async (baseDir) => { + const result = await window.vdd.prefs.setBaseDir(baseDir) + set({ prefs: result.prefs }) + return result + }, + + setMaximized: (value) => set({ maximized: value }) +})) diff --git a/VirtualDriverControl/src/renderer/src/styles/global.css b/VirtualDriverControl/src/renderer/src/styles/global.css new file mode 100644 index 00000000..da790e2c --- /dev/null +++ b/VirtualDriverControl/src/renderer/src/styles/global.css @@ -0,0 +1,1936 @@ +/* =========================================================================== + Virtual Driver Control - design system + WinUI 3 / Fluent 2 inspired. Mica backdrop on Win11, layered surfaces, + accent-pill navigation, Fluent control fills and strokes. + =========================================================================== */ + +:root { + /* Accent is set at runtime from preferences; Windows accent fallback. */ + --accent: #4cc2ff; + --accent-soft: color-mix(in srgb, var(--accent) 20%, transparent); + --accent-softer: color-mix(in srgb, var(--accent) 11%, transparent); + --accent-strong: color-mix(in srgb, var(--accent) 85%, white); + --accent-text: color-mix(in srgb, var(--accent) 80%, white); + + --font-ui: 'Segoe UI Variable Text', 'Segoe UI', system-ui, sans-serif; + --font-display: 'Segoe UI Variable Display', 'Segoe UI', system-ui, sans-serif; + --font-mono: 'Cascadia Mono', 'Cascadia Code', Consolas, monospace; + + --titlebar-h: 48px; + --sidebar-w: 244px; + /* Fluent corner radii: controls ~4-5, surfaces/cards 8 */ + --radius-sm: 5px; + --radius: 8px; + --radius-lg: 8px; + + --ease-snap: cubic-bezier(0.2, 0.9, 0.25, 1); +} + +[data-theme='dark'] { + color-scheme: dark; + --bg: #202020; /* mica fallback tint */ + --bg-deep: #181818; + --layer: rgba(255, 255, 255, 0.034); /* content layer over mica */ + --surface: rgba(255, 255, 255, 0.051); /* card fill */ + --surface-2: rgba(255, 255, 255, 0.062); /* control fill */ + --surface-3: rgba(255, 255, 255, 0.088); /* control hover fill */ + --surface-solid: #2b2b2b; + --border: rgba(255, 255, 255, 0.067); /* card stroke */ + --border-strong: rgba(255, 255, 255, 0.14); + --control-strong: rgba(255, 255, 255, 0.45); /* textbox underline, toggle ring */ + --text: #ffffff; + --text-dim: rgba(255, 255, 255, 0.69); + --text-faint: rgba(255, 255, 255, 0.44); + --success: #6ccb5f; + --warning: #f9ce4d; + --danger: #ff99a4; + --info: #60cdff; + --shadow-card: 0 2px 4px rgba(0, 0, 0, 0.13); + --shadow-pop: 0 8px 16px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.06); + --stage-screen: linear-gradient(150deg, color-mix(in srgb, var(--accent) 24%, #1c2227), #15191d 62%); +} + +[data-theme='light'] { + color-scheme: light; + --bg: #f3f3f3; + --bg-deep: #eaeaea; + --layer: rgba(255, 255, 255, 0.52); + --surface: rgba(255, 255, 255, 0.7); + --surface-2: rgba(255, 255, 255, 0.92); + --surface-3: rgba(0, 0, 0, 0.037); + --surface-solid: #ffffff; + --border: rgba(0, 0, 0, 0.058); + --border-strong: rgba(0, 0, 0, 0.14); + --control-strong: rgba(0, 0, 0, 0.44); + --text: #1b1b1b; + --text-dim: rgba(0, 0, 0, 0.62); + --text-faint: rgba(0, 0, 0, 0.44); + --success: #0f7b0f; + --warning: #9d5d00; + --danger: #c42b1c; + --info: #005fb8; + --shadow-card: 0 2px 4px rgba(0, 0, 0, 0.04); + --shadow-pop: 0 8px 16px rgba(0, 0, 0, 0.14), 0 0 0 1px rgba(0, 0, 0, 0.05); + --stage-screen: linear-gradient(150deg, color-mix(in srgb, var(--accent) 28%, #ffffff), #e8ecef 62%); +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, +body, +#root { + height: 100%; + overflow: hidden; +} + +body { + font-family: var(--font-ui); + font-size: 13.5px; + color: var(--text); + background: var(--bg); + -webkit-font-smoothing: antialiased; + user-select: none; +} + +/* With Mica the window backdrop comes from the OS - keep the page transparent + so the desktop tint shows through the chrome (titlebar + nav pane). */ +body[data-backdrop='mica'] { + background: transparent; +} + +::-webkit-scrollbar { + width: 10px; + height: 10px; +} +::-webkit-scrollbar-thumb { + background: var(--surface-3); + border-radius: 6px; + border: 2px solid transparent; + background-clip: content-box; +} +::-webkit-scrollbar-thumb:hover { + background: var(--border-strong); + border: 2px solid transparent; + background-clip: content-box; +} +::-webkit-scrollbar-corner { + background: transparent; +} + +/* ============================== Layout ================================== */ + +.app-shell { + display: grid; + grid-template-rows: var(--titlebar-h) 1fr; + grid-template-columns: var(--sidebar-w) 1fr; + grid-template-areas: + 'titlebar titlebar' + 'sidebar main'; + height: 100%; + position: relative; +} + +/* WinUI NavigationView content layer: an elevated pane with a rounded + top-left corner sitting on the mica backdrop. */ +.app-main { + grid-area: main; + position: relative; + overflow: hidden; + z-index: 1; + background: var(--layer); + border: 1px solid var(--border); + border-right: none; + border-bottom: none; + border-top-left-radius: var(--radius); +} + +.page-scroll { + height: 100%; + overflow-y: auto; + padding: 26px 30px 48px; +} + +.page-inner { + max-width: 1060px; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 18px; +} + +.page-title { + font-family: var(--font-display); + font-size: 24px; + font-weight: 650; + letter-spacing: -0.02em; +} + +.page-subtitle { + color: var(--text-dim); + margin-top: 4px; + font-size: 13px; +} + +.page-header { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 16px; + margin-bottom: 4px; +} + +/* ============================== Titlebar ================================ */ + +.titlebar { + grid-area: titlebar; + display: flex; + align-items: center; + gap: 12px; + padding-left: 16px; + -webkit-app-region: drag; + background: transparent; + position: relative; + z-index: 5; +} + +.titlebar .brand { + display: flex; + align-items: center; + gap: 10px; + font-family: var(--font-ui); + font-weight: 400; + font-size: 12.5px; + letter-spacing: 0.01em; +} + +.brand-mark { + width: 22px; + height: 22px; + object-fit: contain; +} + +.titlebar .titlebar-status { + display: flex; + align-items: center; + gap: 7px; + font-size: 12px; + color: var(--text-dim); + -webkit-app-region: no-drag; + padding: 4px 11px; + border-radius: 99px; + border: 1px solid var(--border); + background: var(--surface); +} + +.titlebar-spacer { + flex: 1; +} + +/* Win11 caption buttons */ +.window-controls { + display: flex; + height: 100%; + -webkit-app-region: no-drag; +} + +.window-controls button { + width: 46px; + height: 100%; + border: none; + background: transparent; + color: var(--text); + display: grid; + place-items: center; + cursor: default; + transition: background 0.1s; +} + +.window-controls button:hover { + background: var(--surface-3); +} + +.window-controls button.close:hover { + background: #c42b1c; + color: #fff; +} + +.window-controls button.close:active { + background: #b1271b; +} + +/* ============================== Sidebar ================================= */ + +/* WinUI NavigationView pane: transparent over mica, selection pill on the + active item. */ +.sidebar { + grid-area: sidebar; + display: flex; + flex-direction: column; + padding: 8px 8px 14px; + gap: 2px; + background: transparent; + position: relative; + z-index: 2; +} + +.nav-item { + display: flex; + align-items: center; + gap: 12px; + padding: 9px 12px; + border-radius: var(--radius-sm); + border: none; + background: transparent; + color: var(--text); + font: inherit; + font-weight: 400; + text-align: left; + cursor: default; + position: relative; + transition: background 0.1s, color 0.1s; +} + +.nav-item svg { + color: var(--text-dim); + transition: color 0.1s; +} + +.nav-item:hover { + background: var(--surface-2); +} + +.nav-item:active { + background: var(--surface); + color: var(--text-dim); +} + +.nav-item.active { + background: var(--surface-3); + font-weight: 600; +} + +.nav-item.active svg { + color: var(--accent-text); +} + +.nav-item.active::before { + content: ''; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 3px; + height: 16px; + border-radius: 2px; + background: var(--accent); +} + +.nav-section { + margin: 12px 12px 5px; + font-size: 10.5px; + font-weight: 650; + text-transform: uppercase; + letter-spacing: 0.09em; + color: var(--text-faint); +} + +.sidebar-footer { + margin-top: auto; + padding: 10px 12px 4px; + font-size: 11px; + color: var(--text-faint); + display: flex; + flex-direction: column; + gap: 4px; +} + +/* ============================== Cards =================================== */ + +.card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow-card); + overflow: hidden; +} + +.card-header { + display: flex; + align-items: center; + gap: 11px; + padding: 15px 18px 0; +} + +.card-header .card-icon { + width: 30px; + height: 30px; + border-radius: 6px; + display: grid; + place-items: center; + background: var(--accent-softer); + color: var(--accent-text); + flex-shrink: 0; +} + +.card-header h3 { + font-size: 14.5px; + font-weight: 620; + font-family: var(--font-display); +} + +.card-header .card-sub { + font-size: 12px; + color: var(--text-dim); + margin-top: 1px; +} + +.card-header .card-actions { + margin-left: auto; + display: flex; + gap: 8px; + align-items: center; +} + +.card-body { + padding: 15px 18px 18px; +} + +.card-grid { + display: grid; + gap: 16px; +} + +.card-grid.two { + grid-template-columns: 1fr 1fr; +} + +@media (max-width: 1100px) { + .card-grid.two { + grid-template-columns: 1fr; + } +} + +/* ============================== Buttons ================================= */ + +/* Fluent Button: subtle fill, 1px stroke, pressed state dims content */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 7px; + padding: 6px 14px 7px; + border-radius: var(--radius-sm); + border: 1px solid var(--border); + background: var(--surface-2); + color: var(--text); + font: inherit; + font-weight: 400; + cursor: default; + white-space: nowrap; + transition: background 0.1s, border-color 0.1s, color 0.1s; +} + +.btn:hover:not(:disabled) { + background: var(--surface-3); +} + +.btn:active:not(:disabled) { + background: var(--surface); + color: var(--text-dim); +} + +.btn:disabled { + opacity: 0.4; +} + +.btn.primary { + background: var(--accent); + border-color: color-mix(in srgb, var(--accent) 86%, black); + color: rgba(0, 0, 0, 0.87); + font-weight: 600; +} + +.btn.primary:hover:not(:disabled) { + background: color-mix(in srgb, var(--accent) 90%, var(--bg)); +} + +.btn.primary:active:not(:disabled) { + background: color-mix(in srgb, var(--accent) 80%, var(--bg)); + color: rgba(0, 0, 0, 0.62); +} + +.btn.danger { + background: color-mix(in srgb, var(--danger) 14%, transparent); + border-color: color-mix(in srgb, var(--danger) 38%, transparent); + color: var(--danger); +} + +.btn.danger:hover:not(:disabled) { + background: color-mix(in srgb, var(--danger) 24%, transparent); +} + +.btn.ghost { + background: transparent; + border-color: transparent; + color: var(--text-dim); +} + +.btn.ghost:hover:not(:disabled) { + background: var(--surface-2); + color: var(--text); +} + +.btn.small { + padding: 4px 10px; + font-size: 12.5px; + border-radius: 6px; +} + +.btn.icon-only { + padding: 7px; +} + +/* ============================== Inputs ================================== */ + +.field { + display: flex; + flex-direction: column; + gap: 6px; + min-width: 0; +} + +.field > label { + font-size: 12px; + font-weight: 560; + color: var(--text-dim); +} + +.field .hint { + font-size: 11.5px; + color: var(--text-faint); +} + +/* Fluent TextBox: subtle fill with a strong bottom hairline that turns into + a 2px accent underline on focus. */ +input[type='text'], +input[type='number'], +select, +textarea { + font: inherit; + color: var(--text); + background: var(--surface-2); + border: 1px solid var(--border); + border-bottom-color: var(--control-strong); + border-radius: var(--radius-sm); + padding: 7px 10px; + outline: none; + width: 100%; + transition: border-color 0.1s, background 0.1s, box-shadow 0.1s; +} + +input:hover:not(:focus), +select:hover:not(:focus), +textarea:hover:not(:focus) { + background: var(--surface-3); +} + +input:focus, +select:focus, +textarea:focus { + background: var(--surface); + border-bottom-color: var(--accent); + box-shadow: inset 0 -1px 0 var(--accent); +} + +input[type='number']::-webkit-inner-spin-button { + opacity: 0.4; +} + +select option { + background: var(--surface-solid); + color: var(--text); +} + +input[type='range'] { + accent-color: var(--accent); + width: 100%; +} + +input[type='color'] { + appearance: none; + border: 1px solid var(--border-strong); + border-radius: 6px; + width: 34px; + height: 26px; + padding: 2px; + background: var(--surface-2); +} + +/* Toggle switch */ +.toggle { + display: inline-flex; + align-items: center; + gap: 10px; + cursor: default; +} + +/* WinUI ToggleSwitch: outlined track when off, accent fill when on */ +.toggle .track { + width: 40px; + height: 20px; + border-radius: 99px; + background: transparent; + border: 1px solid var(--control-strong); + position: relative; + transition: background 0.15s, border-color 0.15s; + flex-shrink: 0; +} + +.toggle .knob { + position: absolute; + top: 3px; + left: 3px; + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--text-dim); + transition: transform 0.18s var(--ease-snap), background 0.15s, width 0.1s, height 0.1s, top 0.1s; +} + +.toggle:hover .knob { + width: 14px; + height: 14px; + top: 2px; +} + +.toggle.on .track { + background: var(--accent); + border-color: var(--accent); +} + +.toggle.on .knob { + transform: translateX(20px); + background: var(--bg); +} + +.toggle.on:hover .knob { + transform: translateX(19px); +} + +.toggle.disabled { + opacity: 0.45; + pointer-events: none; +} + +.toggle .toggle-label { + font-weight: 530; +} + +.toggle .toggle-sub { + font-size: 11.5px; + color: var(--text-faint); + display: block; +} + +/* Segmented control - WinUI SelectorBar with accent underline */ +.segmented { + display: inline-flex; + background: var(--surface-2); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 3px; + gap: 2px; +} + +.segmented button { + position: relative; + border: none; + background: transparent; + color: var(--text-dim); + font: inherit; + font-size: 12.5px; + font-weight: 400; + padding: 5px 12px; + border-radius: 4px; + cursor: default; + transition: background 0.1s, color 0.1s; +} + +.segmented button:hover { + background: var(--surface-2); + color: var(--text); +} + +.segmented button.active { + background: var(--surface-3); + color: var(--text); + font-weight: 600; +} + +.segmented button.active::after { + content: ''; + position: absolute; + left: 25%; + right: 25%; + bottom: 0; + height: 3px; + border-radius: 2px; + background: var(--accent); +} + +/* Chips */ +.chip { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + border-radius: 99px; + background: var(--surface-2); + border: 1px solid var(--border); + font-size: 12px; + font-weight: 550; + color: var(--text-dim); + transition: all 0.12s; +} + +.chip.selectable { + cursor: default; +} + +.chip.selectable:hover { + border-color: var(--border-strong); + color: var(--text); +} + +.chip.on { + background: var(--accent-soft); + border-color: color-mix(in srgb, var(--accent) 35%, transparent); + color: var(--accent-text); +} + +.chip .chip-x { + display: grid; + place-items: center; + opacity: 0.6; +} + +.chip .chip-x:hover { + opacity: 1; +} + +/* Badges */ +.badge { + display: inline-flex; + align-items: center; + gap: 5px; + font-size: 11px; + font-weight: 620; + letter-spacing: 0.02em; + padding: 3px 8px; + border-radius: 99px; +} + +.badge.ok { + background: color-mix(in srgb, var(--success) 14%, transparent); + color: var(--success); +} + +.badge.warn { + background: color-mix(in srgb, var(--warning) 14%, transparent); + color: var(--warning); +} + +.badge.err { + background: color-mix(in srgb, var(--danger) 14%, transparent); + color: var(--danger); +} + +.badge.neutral { + background: var(--surface-2); + color: var(--text-dim); +} + +/* ============================== Status orb =============================== */ + +.status-orb { + position: relative; + width: 9px; + height: 9px; + border-radius: 50%; + flex-shrink: 0; +} + +.status-orb.online { + background: var(--success); +} + +.status-orb.online::after { + content: ''; + position: absolute; + inset: -4px; + border-radius: 50%; + border: 1.5px solid var(--success); + animation: orb-pulse 2.2s ease-out infinite; +} + +.status-orb.offline { + background: var(--warning); +} + +.status-orb.missing { + background: var(--danger); +} + +.status-orb.unknown { + background: var(--text-faint); +} + +@keyframes orb-pulse { + 0% { + transform: scale(0.6); + opacity: 0.9; + } + 80% { + transform: scale(1.7); + opacity: 0; + } + 100% { + opacity: 0; + } +} + +/* ============================== Toasts =================================== */ + +.toast-stack { + position: fixed; + bottom: 18px; + right: 18px; + display: flex; + flex-direction: column; + gap: 9px; + z-index: 100; + width: 340px; +} + +.toast { + display: flex; + gap: 11px; + align-items: flex-start; + background: color-mix(in srgb, var(--surface-solid) 92%, transparent); + border: 1px solid var(--border-strong); + border-radius: var(--radius); + box-shadow: var(--shadow-pop); + padding: 12px 14px; + backdrop-filter: blur(18px); +} + +.toast .toast-icon { + margin-top: 1px; + flex-shrink: 0; +} + +.toast.success .toast-icon { + color: var(--success); +} +.toast.error .toast-icon { + color: var(--danger); +} +.toast.info .toast-icon { + color: var(--info); +} +.toast.warning .toast-icon { + color: var(--warning); +} + +.toast .toast-title { + font-weight: 620; + font-size: 13px; +} + +.toast .toast-message { + color: var(--text-dim); + font-size: 12.5px; + margin-top: 2px; + word-break: break-word; + user-select: text; +} + +/* ============================== Modal ===================================== */ + +.modal-backdrop { + position: fixed; + inset: 0; + background: rgba(2, 4, 8, 0.55); + backdrop-filter: blur(4px); + z-index: 60; + display: grid; + place-items: center; + padding: 40px; +} + +.modal { + width: min(860px, 100%); + max-height: calc(100vh - 120px); + display: flex; + flex-direction: column; + background: var(--surface-solid); + border: 1px solid var(--border-strong); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-pop); + overflow: hidden; +} + +.modal-header { + display: flex; + align-items: center; + gap: 10px; + padding: 16px 20px; + border-bottom: 1px solid var(--border); +} + +.modal-header h3 { + font-family: var(--font-display); + font-size: 16px; +} + +.modal-body { + padding: 18px 20px; + overflow-y: auto; +} + +.modal-footer { + display: flex; + justify-content: flex-end; + gap: 9px; + padding: 14px 20px; + border-top: 1px solid var(--border); +} + +/* ============================== Display canvas =========================== */ + +.count-stepper { + display: inline-flex; + align-items: center; + gap: 14px; + background: var(--surface-2); + border: 1px solid var(--border-strong); + border-radius: 99px; + padding: 6px 8px; +} + +.count-stepper .count-value { + font-family: var(--font-display); + font-size: 22px; + font-weight: 700; + min-width: 44px; + text-align: center; + font-variant-numeric: tabular-nums; +} + +.count-stepper button { + width: 34px; + height: 34px; + border-radius: 50%; + border: 1px solid var(--border-strong); + background: var(--surface-2); + color: var(--text); + display: grid; + place-items: center; + cursor: default; + transition: all 0.12s; +} + +.count-stepper button:hover:not(:disabled) { + background: var(--accent-soft); + border-color: var(--accent); + color: var(--accent-text); +} + +.count-stepper button:disabled { + opacity: 0.35; +} + +.count-stepper.compact { + padding: 3px 5px; + gap: 8px; +} + +.count-stepper.compact .count-value { + font-size: 15px; + min-width: 26px; +} + +.count-stepper.compact button { + width: 26px; + height: 26px; +} + +/* Stat tiles */ +.stat-tiles { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); + gap: 12px; +} + +.stat-tile { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 13px 15px; + display: flex; + flex-direction: column; + gap: 5px; + min-width: 0; +} + +.stat-tile .stat-label, +.lifecycle-block .stat-label { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.07em; + color: var(--text-faint); + font-weight: 620; + display: flex; + align-items: center; + gap: 6px; +} + +.stat-tile .stat-value, +.lifecycle-block .stat-value { + font-family: var(--font-display); + font-size: 16px; + font-weight: 640; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.stat-tile .stat-sub, +.lifecycle-block .stat-sub { + font-size: 11.5px; + color: var(--text-dim); +} + +/* Quick toggle tiles */ +.quick-toggles { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 12px; +} + +.quick-toggle { + display: flex; + align-items: center; + gap: 12px; + padding: 13px 15px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + transition: border-color 0.15s, background 0.15s; +} + +.quick-toggle.on { + border-color: color-mix(in srgb, var(--accent) 38%, transparent); + background: var(--accent-softer); +} + +.quick-toggle .qt-icon { + width: 34px; + height: 34px; + border-radius: 9px; + display: grid; + place-items: center; + background: var(--surface-2); + color: var(--text-dim); + flex-shrink: 0; + transition: all 0.15s; +} + +.quick-toggle.on .qt-icon { + background: var(--accent-soft); + color: var(--accent-text); +} + +.quick-toggle .qt-text { + flex: 1; + min-width: 0; +} + +.quick-toggle .qt-title { + font-weight: 580; + font-size: 13px; +} + +.quick-toggle .qt-sub { + font-size: 11.5px; + color: var(--text-faint); +} + +/* ============================== Tables / lists =========================== */ + +.data-table { + width: 100%; + border-collapse: collapse; + font-size: 12.5px; +} + +.data-table th { + text-align: left; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-faint); + font-weight: 640; + padding: 7px 10px; + border-bottom: 1px solid var(--border); +} + +.data-table td { + padding: 7px 10px; + border-bottom: 1px solid var(--border); + color: var(--text-dim); +} + +.data-table tr:last-child td { + border-bottom: none; +} + +.data-table td:first-child { + color: var(--text); + font-weight: 540; +} + +/* Resolution rows */ +.res-row { + display: flex; + align-items: center; + gap: 12px; + padding: 11px 14px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface); + transition: border-color 0.13s; +} + +.res-row:hover { + border-color: var(--border-strong); +} + +.res-row .res-dim { + font-family: var(--font-display); + font-weight: 650; + font-size: 15px; + min-width: 120px; + font-variant-numeric: tabular-nums; +} + +.res-row .res-aspect { + font-size: 11px; + color: var(--text-faint); + min-width: 44px; +} + +.res-row .res-rates { + flex: 1; + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +/* ============================== Console ================================== */ + +.console-feed { + font-family: var(--font-mono); + font-size: 12px; + line-height: 1.65; + background: var(--bg-deep); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 10px 0; + overflow-y: auto; + user-select: text; +} + +.console-line { + display: flex; + gap: 10px; + padding: 0 14px; + white-space: pre-wrap; + word-break: break-all; +} + +.console-line:hover { + background: var(--surface); +} + +.console-line .ts { + color: var(--text-faint); + flex-shrink: 0; +} + +.console-line .src { + flex-shrink: 0; + width: 38px; + text-align: center; + border-radius: 4px; + font-size: 10px; + font-weight: 700; + align-self: center; + letter-spacing: 0.04em; +} + +.console-line .src.file { + background: color-mix(in srgb, var(--info) 16%, transparent); + color: var(--info); +} + +.console-line .src.pipe { + background: var(--accent-soft); + color: var(--accent-text); +} + +.console-line .src.app { + background: var(--surface-3); + color: var(--text-dim); +} + +.console-line .msg.info { + color: var(--text); +} +.console-line .msg.debug { + color: var(--text-faint); +} +.console-line .msg.warning { + color: var(--warning); +} +.console-line .msg.error { + color: var(--danger); +} + +/* ============================== Diff view ================================ */ + +.diff-view { + font-family: var(--font-mono); + font-size: 11.5px; + line-height: 1.55; + background: var(--bg-deep); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: auto; + user-select: text; +} + +.diff-line { + display: flex; + white-space: pre; +} + +.diff-line .gutter { + width: 26px; + flex-shrink: 0; + text-align: center; + color: var(--text-faint); + user-select: none; +} + +.diff-line.add { + background: color-mix(in srgb, var(--success) 11%, transparent); + color: var(--success); +} + +.diff-line.del { + background: color-mix(in srgb, var(--danger) 11%, transparent); + color: var(--danger); +} + +.diff-line.ctx { + color: var(--text-dim); +} + +/* ============================== Misc ===================================== */ + +.banner { + display: flex; + align-items: center; + gap: 11px; + padding: 11px 15px; + border-radius: var(--radius); + border: 1px solid; + font-size: 12.5px; +} + +.banner.warn { + background: color-mix(in srgb, var(--warning) 9%, transparent); + border-color: color-mix(in srgb, var(--warning) 30%, transparent); + color: var(--warning); +} + +.banner.info { + background: color-mix(in srgb, var(--info) 9%, transparent); + border-color: color-mix(in srgb, var(--info) 30%, transparent); + color: var(--info); +} + +.row { + display: flex; + align-items: center; + gap: 10px; +} + +.row.wrap { + flex-wrap: wrap; +} + +.row.between { + justify-content: space-between; +} + +.grid-2 { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 14px; +} + +.grid-3 { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 14px; +} + +.grid-4 { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 14px; +} + +.divider { + height: 1px; + background: var(--border); + margin: 14px 0; +} + +.section-label { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.07em; + color: var(--text-faint); + font-weight: 620; + margin-bottom: 10px; +} + + +.muted { + color: var(--text-dim); +} + +.faint { + color: var(--text-faint); +} + +.mono { + font-family: var(--font-mono); +} + +.selectable-text { + user-select: text; +} + +.spin { + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.dropzone { + border: 1.5px dashed var(--border-strong); + border-radius: var(--radius-lg); + padding: 38px 24px; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + color: var(--text-dim); + transition: all 0.15s; +} + +.dropzone.over, +.dropzone:hover { + border-color: var(--accent); + background: var(--accent-softer); + color: var(--accent-text); +} + +.preset-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 9px; +} + +.preset-card { + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--surface); + padding: 10px 12px; + text-align: left; + font: inherit; + color: var(--text); + cursor: default; + transition: all 0.13s; +} + +.preset-card:hover:not(:disabled) { + border-color: var(--accent); + background: var(--accent-softer); +} + +.preset-card:disabled { + opacity: 0.45; +} + +.preset-card .p-dim { + font-weight: 640; + font-variant-numeric: tabular-nums; + font-size: 13px; +} + +.preset-card .p-label { + font-size: 11px; + color: var(--text-faint); + margin-top: 2px; +} + +.gpu-card { + display: flex; + align-items: center; + gap: 14px; + padding: 15px 17px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface); + transition: border-color 0.14s; +} + +.gpu-card.assigned { + border-color: color-mix(in srgb, var(--accent) 45%, transparent); + background: var(--accent-softer); +} + +.gpu-card .gpu-icon { + width: 40px; + height: 40px; + border-radius: 11px; + display: grid; + place-items: center; + background: var(--surface-2); + color: var(--text-dim); + flex-shrink: 0; +} + +.gpu-card.assigned .gpu-icon { + background: var(--accent-soft); + color: var(--accent-text); +} + +.backup-row { + display: flex; + align-items: center; + gap: 12px; + padding: 9px 13px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--surface); + font-size: 12.5px; +} + +.size-compare { + position: relative; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--bg-deep); + height: 220px; + overflow: hidden; +} + +.size-compare .sc-rect { + position: absolute; + bottom: 14px; + left: 14px; + border: 1.5px solid; + border-radius: 3px; + display: flex; + align-items: flex-start; + justify-content: flex-end; +} + +.size-compare .sc-tag { + font-size: 9.5px; + padding: 1px 5px; + font-weight: 600; + font-variant-numeric: tabular-nums; +} + +.kbd { + font-family: var(--font-mono); + font-size: 11px; + background: var(--surface-2); + border: 1px solid var(--border-strong); + border-bottom-width: 2px; + border-radius: 5px; + padding: 1px 6px; + color: var(--text-dim); +} + +.save-bar { + position: absolute; + bottom: 18px; + left: 0; + right: 0; + margin: 0 auto; + width: fit-content; + display: flex; + align-items: center; + gap: 9px; + padding: 8px 10px 8px 18px; + border-radius: 99px; + background: color-mix(in srgb, var(--surface-solid) 90%, transparent); + border: 1px solid var(--border-strong); + box-shadow: var(--shadow-pop); + backdrop-filter: blur(18px); + z-index: 40; +} + +.save-bar .save-bar-text { + font-size: 12.5px; + font-weight: 580; + color: var(--text-dim); + margin-right: 6px; +} + +/* --- Driver lifecycle card ---------------------------------------------- */ + +.lifecycle-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 12px; +} + +.lifecycle-block { + display: flex; + flex-direction: column; + gap: 3px; + padding: 12px 14px; + border-radius: var(--radius-sm); + background: var(--surface-2); + border: 1px solid var(--border); +} + +.lifecycle-progress { + display: flex; + flex-direction: column; + gap: 6px; + margin-top: 12px; +} + +.progress-track { + height: 6px; + border-radius: 99px; + background: var(--surface-2); + border: 1px solid var(--border); + overflow: hidden; + position: relative; +} + +.progress-fill { + height: 100%; + border-radius: 99px; + background: linear-gradient(90deg, var(--accent), color-mix(in srgb, var(--accent) 60%, #fff)); + transition: width 0.25s ease; +} + +.progress-fill.indeterminate { + position: absolute; + width: 36%; + animation: progress-slide 1.2s ease-in-out infinite; +} + +@keyframes progress-slide { + 0% { + left: -36%; + } + 100% { + left: 100%; + } +} + +.progress-message { + font-size: 11.5px; + color: var(--text-dim); + font-variant-numeric: tabular-nums; +} + +.lifecycle-actions { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; + margin-top: 12px; +} + +.lifecycle-hint { + font-size: 11.5px; + font-weight: 600; + color: var(--warning); + margin-left: 4px; +} + +/* --- Audio endpoints ------------------------------------------------------ */ + +.endpoint-list { + display: flex; + flex-direction: column; + gap: 7px; +} + +.endpoint-row { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 9px 11px; + border-radius: var(--radius-sm); + background: var(--surface); + border: 1px solid var(--border); +} + +.endpoint-row.virtual { + border-color: color-mix(in srgb, var(--accent) 36%, transparent); + background: var(--accent-softer); +} + +.ep-icon { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 8px; + background: var(--surface-2); + color: var(--text-dim); + flex-shrink: 0; + margin-top: 1px; +} + +.endpoint-row.virtual .ep-icon { + color: var(--accent-text); +} + +.ep-main { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 6px; +} + +.ep-name { + font-size: 12.5px; + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ep-badge { + display: inline-block; + margin-left: 6px; + padding: 1px 6px; + border-radius: 99px; + font-size: 9.5px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + background: color-mix(in srgb, var(--success) 16%, transparent); + color: var(--success); + vertical-align: 1px; +} + +.ep-badge.dim { + background: var(--surface-2); + color: var(--text-faint); +} + +.ep-badge.accent { + background: var(--accent-soft); + color: var(--accent-text); +} + +.ep-controls { + display: flex; + align-items: center; + gap: 8px; +} + +.ep-controls input[type='range'] { + flex: 1; + min-width: 60px; + accent-color: var(--accent); +} + +.ep-vol { + font-size: 11px; + color: var(--text-dim); + width: 36px; + text-align: right; + flex-shrink: 0; +} + +/* --- Audio routing -------------------------------------------------------- */ + +.route-add { + display: flex; + align-items: center; + gap: 9px; + margin-bottom: 14px; +} + +.route-add select { + flex: 1; + min-width: 0; +} + +.route-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.route-row { + display: flex; + align-items: flex-start; + gap: 11px; + padding: 10px 12px; + border-radius: var(--radius-sm); + background: var(--surface); + border: 1px solid var(--border); +} + +.route-row.active { + border-color: color-mix(in srgb, var(--accent) 32%, transparent); +} + +.route-main { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 7px; +} + +.route-path { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.route-ep { + display: inline-flex; + align-items: center; + gap: 5px; + font-size: 12px; + font-weight: 600; + padding: 2px 8px; + border-radius: 99px; + background: var(--surface-2); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 46%; +} + +.route-controls { + display: flex; + align-items: center; + gap: 8px; +} + +.route-controls input[type='range'] { + width: 110px; + accent-color: var(--accent); +} + +.route-meter { + flex: 1; + height: 5px; + min-width: 50px; + border-radius: 99px; + background: var(--surface-2); + overflow: hidden; +} + +.route-meter.idle { + opacity: 0.35; +} + +.route-meter-fill { + height: 100%; + width: 0%; + border-radius: 99px; + background: linear-gradient(90deg, var(--success), var(--warning) 85%, var(--danger)); + transition: width 60ms linear; +} + +.route-error { + font-size: 11px; + color: var(--danger); +} + +/* --- Desktop arrangement canvas ------------------------------------------- */ + +.arrange-stage { + position: relative; + border-radius: var(--radius); + background-color: var(--bg-deep); + background-image: + radial-gradient(circle at 1px 1px, var(--border) 1px, transparent 1.5px), + radial-gradient(ellipse 70% 90% at 50% -20%, color-mix(in srgb, var(--accent) 6%, transparent), transparent); + background-size: + 20px 20px, + 100% 100%; + border: 1px solid var(--border); + overflow: hidden; + max-width: 100%; +} + +.canvas-empty { + position: absolute; + inset: 0; + display: grid; + place-items: center; +} + +.canvas-busy { + position: absolute; + inset: 0; + display: grid; + place-items: center; + background: color-mix(in srgb, var(--bg) 55%, transparent); + backdrop-filter: blur(3px); + z-index: 3; + border-radius: inherit; +} + +.arrange-display { + position: absolute; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 1px; + padding: 4px; + border-radius: 6px; + border: 1px solid var(--border-strong); + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.05), transparent 45%), + var(--stage-screen); + box-shadow: + inset 0 0 0 1px rgba(0, 0, 0, 0.25), + 0 4px 14px rgba(0, 0, 0, 0.3); + overflow: hidden; + transition: border-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease; +} + +.arrange-display:hover { + border-color: var(--text-dim); + transform: translateY(-1px); +} + +.arrange-display.virtual { + border-color: color-mix(in srgb, var(--accent) 70%, transparent); + box-shadow: + inset 0 0 0 1px rgba(0, 0, 0, 0.25), + 0 4px 14px rgba(0, 0, 0, 0.3), + 0 0 16px var(--accent-softer); +} + +/* Canvas footer: legend + virtual display count control */ +.canvas-bar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; + margin-top: 12px; +} + +.canvas-legend { + display: flex; + align-items: center; + gap: 7px; + flex-wrap: wrap; +} + +.canvas-hint { + font-size: 11.5px; + color: var(--text-faint); +} + +.canvas-controls { + display: flex; + align-items: center; + gap: 9px; +} + +.canvas-ctl-label { + font-size: 12px; + font-weight: 600; + color: var(--text-dim); +} + +.ad-name { + display: flex; + align-items: center; + gap: 4px; + font-size: 10.5px; + font-weight: 650; + max-width: 95%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ad-name svg { + color: var(--warning); + flex-shrink: 0; +} + +.ad-res { + font-size: 10px; + color: var(--text-dim); +} + +.ad-meta { + font-size: 9px; + color: var(--text-faint); +} + +.ad-badge { + position: absolute; + top: 4px; + right: 5px; + font-size: 8px; + font-weight: 800; + letter-spacing: 0.08em; + padding: 1px 5px; + border-radius: 99px; + background: var(--accent-soft); + color: var(--accent-text); +} diff --git a/VirtualDriverControl/src/renderer/src/utils/audio-router.ts b/VirtualDriverControl/src/renderer/src/utils/audio-router.ts new file mode 100644 index 00000000..304fea0e --- /dev/null +++ b/VirtualDriverControl/src/renderer/src/utils/audio-router.ts @@ -0,0 +1,126 @@ +import { SYSTEM_AUDIO_SOURCE, type AudioRoute } from '@shared/types' + +interface ActiveRoute { + context: AudioContext + stream: MediaStream + gainNode: GainNode + analyser: AnalyserNode + element: HTMLAudioElement + levelBuffer: Uint8Array +} + +/** + * In-app audio pump: captures a source (any microphone/virtual capture device, + * or the system output via WASAPI loopback) and plays it to any output device + * through WebAudio. Routes live for as long as the app runs. + */ +class AudioRouter { + private active = new Map() + private onEnded: ((routeId: string) => void) | null = null + + setOnEnded(handler: (routeId: string) => void): void { + this.onEnded = handler + } + + isActive(routeId: string): boolean { + return this.active.has(routeId) + } + + async start(route: AudioRoute): Promise { + this.stop(route.id) + + let stream: MediaStream + if (route.sourceId === SYSTEM_AUDIO_SOURCE) { + // Main process answers this with a screen source + 'loopback' audio. + stream = await navigator.mediaDevices.getDisplayMedia({ audio: true, video: true }) + for (const track of stream.getVideoTracks()) track.stop() + if (stream.getAudioTracks().length === 0) { + for (const track of stream.getTracks()) track.stop() + throw new Error('System audio loopback is unavailable on this system') + } + } else { + stream = await navigator.mediaDevices.getUserMedia({ + audio: { + deviceId: { exact: route.sourceId }, + echoCancellation: false, + noiseSuppression: false, + autoGainControl: false + } + }) + } + + const context = new AudioContext({ latencyHint: 'interactive' }) + const source = context.createMediaStreamSource(stream) + const gainNode = context.createGain() + gainNode.gain.value = route.gain + const analyser = context.createAnalyser() + analyser.fftSize = 256 + const destination = context.createMediaStreamDestination() + source.connect(gainNode) + gainNode.connect(analyser) + analyser.connect(destination) + + const element = new Audio() + element.srcObject = destination.stream + element.autoplay = true + try { + await element.setSinkId(route.sinkId) + await element.play() + } catch (error) { + for (const track of stream.getTracks()) track.stop() + void context.close() + throw error instanceof Error ? error : new Error(String(error)) + } + + const audioTrack = stream.getAudioTracks()[0] + audioTrack.addEventListener('ended', () => { + if (this.active.has(route.id)) { + this.stop(route.id) + this.onEnded?.(route.id) + } + }) + + this.active.set(route.id, { + context, + stream, + gainNode, + analyser, + element, + levelBuffer: new Uint8Array(analyser.frequencyBinCount) + }) + } + + stop(routeId: string): void { + const entry = this.active.get(routeId) + if (!entry) return + this.active.delete(routeId) + for (const track of entry.stream.getTracks()) track.stop() + entry.element.pause() + entry.element.srcObject = null + void entry.context.close().catch(() => undefined) + } + + stopAll(): void { + for (const id of [...this.active.keys()]) this.stop(id) + } + + setGain(routeId: string, gain: number): void { + const entry = this.active.get(routeId) + if (entry) entry.gainNode.gain.value = gain + } + + /** Current RMS level 0..1 for the route's signal (for meters). */ + level(routeId: string): number { + const entry = this.active.get(routeId) + if (!entry) return 0 + entry.analyser.getByteTimeDomainData(entry.levelBuffer) + let sum = 0 + for (const sample of entry.levelBuffer) { + const centered = (sample - 128) / 128 + sum += centered * centered + } + return Math.min(1, Math.sqrt(sum / entry.levelBuffer.length) * 2.5) + } +} + +export const audioRouter = new AudioRouter() diff --git a/VirtualDriverControl/src/renderer/src/utils/diff.ts b/VirtualDriverControl/src/renderer/src/utils/diff.ts new file mode 100644 index 00000000..d069389d --- /dev/null +++ b/VirtualDriverControl/src/renderer/src/utils/diff.ts @@ -0,0 +1,66 @@ +export interface DiffLine { + type: 'add' | 'del' | 'ctx' + text: string +} + +/** Simple line-based LCS diff - plenty for config-file sized inputs. */ +export function diffLines(before: string, after: string): DiffLine[] { + const a = before.split(/\r?\n/) + const b = after.split(/\r?\n/) + const n = a.length + const m = b.length + + // LCS table (n+1 x m+1). + const lcs: Uint32Array[] = [] + for (let i = 0; i <= n; i++) lcs.push(new Uint32Array(m + 1)) + for (let i = n - 1; i >= 0; i--) { + for (let j = m - 1; j >= 0; j--) { + lcs[i][j] = a[i] === b[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]) + } + } + + const out: DiffLine[] = [] + let i = 0 + let j = 0 + while (i < n && j < m) { + if (a[i] === b[j]) { + out.push({ type: 'ctx', text: a[i] }) + i++ + j++ + } else if (lcs[i + 1][j] >= lcs[i][j + 1]) { + out.push({ type: 'del', text: a[i] }) + i++ + } else { + out.push({ type: 'add', text: b[j] }) + j++ + } + } + while (i < n) out.push({ type: 'del', text: a[i++] }) + while (j < m) out.push({ type: 'add', text: b[j++] }) + return out +} + +/** Collapse long unchanged runs, keeping `context` lines around changes. */ +export function compactDiff(lines: DiffLine[], context = 3): Array { + const keep = new Array(lines.length).fill(false) + lines.forEach((line, idx) => { + if (line.type !== 'ctx') { + for (let k = Math.max(0, idx - context); k <= Math.min(lines.length - 1, idx + context); k++) keep[k] = true + } + }) + const out: Array = [] + let skipped = 0 + lines.forEach((line, idx) => { + if (keep[idx]) { + if (skipped > 0) { + out.push({ type: 'skip', count: skipped }) + skipped = 0 + } + out.push(line) + } else { + skipped++ + } + }) + if (skipped > 0) out.push({ type: 'skip', count: skipped }) + return out +} diff --git a/VirtualDriverControl/src/shared/defaults.ts b/VirtualDriverControl/src/shared/defaults.ts new file mode 100644 index 00000000..cd2c5ab9 --- /dev/null +++ b/VirtualDriverControl/src/shared/defaults.ts @@ -0,0 +1,109 @@ +import type { VddSettings } from './types' + +/** Mirrors the upstream default vdd_settings.xml (safe values everywhere). */ +export const DEFAULT_VDD_SETTINGS: VddSettings = { + monitors: { count: 1 }, + gpu: { friendlyName: 'default' }, + global: { refreshRates: [60, 90, 120, 144, 165, 240] }, + resolutions: [ + { width: 1920, height: 1080, refreshRates: [60] }, + { width: 2560, height: 1440, refreshRates: [60] }, + { width: 3840, height: 2160, refreshRates: [60] } + ], + logging: { + sendLogsThroughPipe: true, + logging: false, + debugLogging: false + }, + colour: { + sdr10bit: false, + hdrPlus: false, + colourFormat: 'RGB' + }, + cursor: { + hardwareCursor: true, + cursorMaxX: 128, + cursorMaxY: 128, + alphaCursorSupport: true, + xorCursorSupportLevel: 2 + }, + edid: { + customEdid: false, + preventSpoof: false, + edidCeaOverride: false + }, + edidIntegration: { + enabled: false, + autoConfigureFromEdid: false, + edidProfilePath: 'EDID/monitor_profile.xml', + overrideManualSettings: false, + fallbackOnError: true + }, + hdrAdvanced: { + hdr10StaticMetadata: { + enabled: false, + maxDisplayMasteringLuminance: 1000.0, + minDisplayMasteringLuminance: 0.05, + maxContentLightLevel: 1000, + maxFrameAvgLightLevel: 400 + }, + colorPrimaries: { + enabled: false, + redX: 0.64, + redY: 0.33, + greenX: 0.3, + greenY: 0.6, + blueX: 0.15, + blueY: 0.06, + whiteX: 0.3127, + whiteY: 0.329 + }, + colorSpace: { + enabled: false, + gammaCorrection: 2.2, + primaryColorSpace: 'sRGB', + enableMatrixTransform: false + } + }, + autoResolutions: { + enabled: false, + sourcePriority: 'manual', + edidModeFiltering: { + minRefreshRate: 24, + maxRefreshRate: 240, + excludeFractionalRates: false, + minResolutionWidth: 640, + minResolutionHeight: 480, + maxResolutionWidth: 7680, + maxResolutionHeight: 4320 + }, + preferredMode: { + useEdidPreferred: false, + fallbackWidth: 1920, + fallbackHeight: 1080, + fallbackRefresh: 60 + } + }, + colorAdvanced: { + bitDepthManagement: { + autoSelectFromColorSpace: false, + forceBitDepth: 8, + fp16SurfaceSupport: true + }, + colorFormatExtended: { + sdrWhiteLevel: 80.0 + } + } +} + +/** Known color space presets for the chromaticity editor. */ +export const COLOR_SPACE_PRESETS: Record< + string, + { redX: number; redY: number; greenX: number; greenY: number; blueX: number; blueY: number; whiteX: number; whiteY: number } +> = { + sRGB: { redX: 0.64, redY: 0.33, greenX: 0.3, greenY: 0.6, blueX: 0.15, blueY: 0.06, whiteX: 0.3127, whiteY: 0.329 }, + 'DCI-P3': { redX: 0.68, redY: 0.32, greenX: 0.265, greenY: 0.69, blueX: 0.15, blueY: 0.06, whiteX: 0.314, whiteY: 0.351 }, + 'Display P3': { redX: 0.68, redY: 0.32, greenX: 0.265, greenY: 0.69, blueX: 0.15, blueY: 0.06, whiteX: 0.3127, whiteY: 0.329 }, + AdobeRGB: { redX: 0.64, redY: 0.33, greenX: 0.21, greenY: 0.71, blueX: 0.15, blueY: 0.06, whiteX: 0.3127, whiteY: 0.329 }, + 'Rec. 2020': { redX: 0.708, redY: 0.292, greenX: 0.17, greenY: 0.797, blueX: 0.131, blueY: 0.046, whiteX: 0.3127, whiteY: 0.329 } +} diff --git a/VirtualDriverControl/src/shared/edid.ts b/VirtualDriverControl/src/shared/edid.ts new file mode 100644 index 00000000..2bab16c4 --- /dev/null +++ b/VirtualDriverControl/src/shared/edid.ts @@ -0,0 +1,445 @@ +import type { EdidChromaticity, EdidHdrMetadata, EdidTiming, ParsedEdid } from './types' + +/** + * Pure-TypeScript EDID parser (base block + CEA-861 extension) and + * IddCx monitor_profile.xml generator. No Node APIs - usable in any process. + */ + +const EDID_HEADER = [0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00] + +// Established timings, bytes 35-37 (bit 7 -> bit 0 per byte). +const ESTABLISHED_TIMINGS: Array<[number, number, number] | null>[] = [ + [ + [720, 400, 70], + [720, 400, 88], + [640, 480, 60], + [640, 480, 67], + [640, 480, 72], + [640, 480, 75], + [800, 600, 56], + [800, 600, 60] + ], + [ + [800, 600, 72], + [800, 600, 75], + [832, 624, 75], + [1024, 768, 87], // interlaced + [1024, 768, 60], + [1024, 768, 70], + [1024, 768, 75], + [1280, 1024, 75] + ], + [[1152, 870, 75], null, null, null, null, null, null, null] +] + +// CEA-861 VIC table (curated, common codes). +const VIC_TABLE: Record = { + 1: [640, 480, 60], + 2: [720, 480, 60], + 3: [720, 480, 60], + 4: [1280, 720, 60], + 5: [1920, 1080, 60, true], + 6: [720, 480, 60, true], + 7: [720, 480, 60, true], + 16: [1920, 1080, 60], + 17: [720, 576, 50], + 18: [720, 576, 50], + 19: [1280, 720, 50], + 20: [1920, 1080, 50, true], + 31: [1920, 1080, 50], + 32: [1920, 1080, 24], + 33: [1920, 1080, 25], + 34: [1920, 1080, 30], + 39: [1920, 1080, 50, true], + 60: [1280, 720, 24], + 61: [1280, 720, 25], + 62: [1280, 720, 30], + 63: [1920, 1080, 120], + 64: [1920, 1080, 100], + 90: [2560, 1080, 60], + 91: [2560, 1080, 100], + 92: [2560, 1080, 120], + 93: [3840, 2160, 24], + 94: [3840, 2160, 25], + 95: [3840, 2160, 30], + 96: [3840, 2160, 50], + 97: [3840, 2160, 60], + 98: [4096, 2160, 24], + 99: [4096, 2160, 25], + 100: [4096, 2160, 30], + 101: [4096, 2160, 50], + 102: [4096, 2160, 60], + 103: [3840, 2160, 24], + 104: [3840, 2160, 25], + 105: [3840, 2160, 30], + 106: [3840, 2160, 50], + 107: [3840, 2160, 60], + 117: [3840, 2160, 100], + 118: [3840, 2160, 120], + 219: [4096, 2160, 100], + 220: [4096, 2160, 120] +} + +const VIDEO_INTERFACES: Record = { + 0: 'Undefined', + 1: 'DVI', + 2: 'HDMI-a', + 3: 'HDMI-b', + 4: 'MDDI', + 5: 'DisplayPort' +} + +function decodeManufacturerId(b0: number, b1: number): string { + const value = (b0 << 8) | b1 + const c1 = ((value >> 10) & 0x1f) + 64 + const c2 = ((value >> 5) & 0x1f) + 64 + const c3 = (value & 0x1f) + 64 + return String.fromCharCode(c1, c2, c3) +} + +function chrom10(high: number, low: number): number { + return Math.round((((high << 2) | low) / 1024) * 10000) / 10000 +} + +function descriptorText(bytes: Uint8Array, offset: number): string { + let text = '' + for (let i = offset + 5; i < offset + 18; i++) { + const ch = bytes[i] + if (ch === 0x0a) break + text += String.fromCharCode(ch) + } + return text.trim() +} + +function parseDtd(bytes: Uint8Array, o: number): EdidTiming | null { + const pixelClock = bytes[o] | (bytes[o + 1] << 8) + if (pixelClock === 0) return null + const hActive = bytes[o + 2] | ((bytes[o + 4] >> 4) << 8) + const hBlank = bytes[o + 3] | ((bytes[o + 4] & 0x0f) << 8) + const vActive = bytes[o + 5] | ((bytes[o + 7] >> 4) << 8) + const vBlank = bytes[o + 6] | ((bytes[o + 7] & 0x0f) << 8) + const interlaced = (bytes[o + 17] & 0x80) !== 0 + const totalPixels = (hActive + hBlank) * (vActive + vBlank) + if (totalPixels === 0 || hActive === 0 || vActive === 0) return null + const refresh = (pixelClock * 10000) / totalPixels + return { + width: hActive, + height: vActive * (interlaced ? 2 : 1), + refreshHz: Math.round(refresh * 1000) / 1000, + interlaced, + source: 'detailed', + pixelClockMHz: Math.round(pixelClock / 100) / 100 + } +} + +function parseCeaBlock(bytes: Uint8Array, base: number, result: ParsedEdid): void { + result.hasCeaExtension = true + const dtdStart = bytes[base + 2] + const flags = bytes[base + 3] + result.ceaBasicAudio = (flags & 0x40) !== 0 + result.ceaYcbcr444 = (flags & 0x20) !== 0 + result.ceaYcbcr422 = (flags & 0x10) !== 0 + + // Data block collection: from base+4 up to dtdStart. + let i = base + 4 + const dataEnd = base + Math.max(dtdStart, 4) + while (i < dataEnd && i < base + 127) { + const header = bytes[i] + const tag = (header >> 5) & 0x07 + const length = header & 0x1f + if (length === 0 && tag === 0) break + if (tag === 2) { + // Video data block - list of VICs + for (let v = 1; v <= length; v++) { + const raw = bytes[i + v] + const vic = raw >= 128 && raw <= 192 ? raw & 0x7f : raw + const native = raw >= 128 && raw <= 192 + const mode = VIC_TABLE[vic] + if (mode) { + result.timings.push({ + width: mode[0], + height: mode[1], + refreshHz: mode[2], + interlaced: mode[3] === true, + source: 'cea-vic', + vic, + native + }) + } + } + } else if (tag === 7 && length >= 2) { + // Extended tag + const extTag = bytes[i + 1] + if (extTag === 6) { + // HDR static metadata data block + const eotf = bytes[i + 2] + const hdr: EdidHdrMetadata = { + eotfSdr: (eotf & 0x01) !== 0, + eotfHdr: (eotf & 0x02) !== 0, + eotfPq: (eotf & 0x04) !== 0, + eotfHlg: (eotf & 0x08) !== 0 + } + // Coded luminance values (CTA-861.3): L = 50 * 2^(cv/32) + if (length >= 4 && bytes[i + 4] > 0) hdr.maxLuminance = Math.round(50 * Math.pow(2, bytes[i + 4] / 32)) + if (length >= 5 && bytes[i + 5] > 0) hdr.maxFrameAvgLuminance = Math.round(50 * Math.pow(2, bytes[i + 5] / 32)) + if (length >= 6 && hdr.maxLuminance) { + const cv = bytes[i + 6] + hdr.minLuminance = Math.round(hdr.maxLuminance * Math.pow(cv / 255, 2) / 100 * 10000) / 10000 + } + result.hdr = hdr + } + } + i += length + 1 + } + + // DTDs in the CEA block. + if (dtdStart >= 4) { + let o = base + dtdStart + while (o + 18 <= base + 127) { + const dtd = parseDtd(bytes, o) + if (!dtd) break + result.timings.push(dtd) + o += 18 + } + } +} + +export function parseEdid(input: Uint8Array): ParsedEdid { + const bytes = input + const result: ParsedEdid = { + valid: false, + errors: [], + manufacturerId: '???', + productCode: 0, + serialNumber: 0, + manufactureWeek: 0, + manufactureYear: 0, + edidVersion: '?', + digital: false, + timings: [], + extensionCount: 0, + hasCeaExtension: false, + checksumOk: false, + rawBytes: bytes.length + } + + if (bytes.length < 128) { + result.errors.push(`EDID must be at least 128 bytes (got ${bytes.length})`) + return result + } + for (let i = 0; i < 8; i++) { + if (bytes[i] !== EDID_HEADER[i]) { + result.errors.push('Invalid EDID header magic') + return result + } + } + + let checksum = 0 + for (let i = 0; i < 128; i++) checksum = (checksum + bytes[i]) & 0xff + result.checksumOk = checksum === 0 + if (!result.checksumOk) result.errors.push('Base block checksum mismatch') + + result.manufacturerId = decodeManufacturerId(bytes[8], bytes[9]) + result.productCode = bytes[10] | (bytes[11] << 8) + result.serialNumber = bytes[12] | (bytes[13] << 8) | (bytes[14] << 16) | (bytes[15] << 24) + result.manufactureWeek = bytes[16] + result.manufactureYear = bytes[17] + 1990 + result.edidVersion = `${bytes[18]}.${bytes[19]}` + + const videoInput = bytes[20] + result.digital = (videoInput & 0x80) !== 0 + if (result.digital) { + const depthCode = (videoInput >> 4) & 0x07 + if (depthCode >= 1 && depthCode <= 6) result.bitDepth = 4 + depthCode * 2 + result.videoInterface = VIDEO_INTERFACES[videoInput & 0x0f] ?? 'Unknown' + } + + if (bytes[21] > 0) result.screenWidthCm = bytes[21] + if (bytes[22] > 0) result.screenHeightCm = bytes[22] + if (bytes[23] !== 0xff) result.gamma = Math.round((bytes[23] + 100) / 100 * 100) / 100 + + result.chromaticity = { + redX: chrom10(bytes[27], (bytes[25] >> 6) & 3), + redY: chrom10(bytes[28], (bytes[25] >> 4) & 3), + greenX: chrom10(bytes[29], (bytes[25] >> 2) & 3), + greenY: chrom10(bytes[30], bytes[25] & 3), + blueX: chrom10(bytes[31], (bytes[26] >> 6) & 3), + blueY: chrom10(bytes[32], (bytes[26] >> 4) & 3), + whiteX: chrom10(bytes[33], (bytes[26] >> 2) & 3), + whiteY: chrom10(bytes[34], bytes[26] & 3) + } + + // Established timings. + for (let byteIdx = 0; byteIdx < 3; byteIdx++) { + const value = bytes[35 + byteIdx] + for (let bit = 0; bit < 8; bit++) { + if ((value & (0x80 >> bit)) !== 0) { + const mode = ESTABLISHED_TIMINGS[byteIdx][bit] + if (mode) { + result.timings.push({ + width: mode[0], + height: mode[1], + refreshHz: mode[2], + interlaced: byteIdx === 1 && bit === 3, + source: 'established' + }) + } + } + } + } + + // Standard timings (bytes 38-53). + for (let i = 0; i < 8; i++) { + const o = 38 + i * 2 + const b1 = bytes[o] + const b2 = bytes[o + 1] + if (b1 === 0x01 && b2 === 0x01) continue + if (b1 === 0x00) continue + const width = (b1 + 31) * 8 + const aspect = (b2 >> 6) & 3 + let height: number + switch (aspect) { + case 0: + height = Math.round((width * 10) / 16) + break + case 1: + height = Math.round((width * 3) / 4) + break + case 2: + height = Math.round((width * 4) / 5) + break + default: + height = Math.round((width * 9) / 16) + } + result.timings.push({ + width, + height, + refreshHz: (b2 & 0x3f) + 60, + source: 'standard' + }) + } + + // 18-byte descriptors (bytes 54-125). + for (let d = 0; d < 4; d++) { + const o = 54 + d * 18 + const isDtd = bytes[o] !== 0 || bytes[o + 1] !== 0 + if (isDtd) { + const dtd = parseDtd(bytes, o) + if (dtd) { + result.timings.push(dtd) + if (!result.preferred) result.preferred = dtd + } + } else { + switch (bytes[o + 3]) { + case 0xfc: + result.displayName = descriptorText(bytes, o) + break + case 0xff: + result.serialString = descriptorText(bytes, o) + break + } + } + } + + // Extension blocks. + result.extensionCount = bytes[126] + for (let ext = 1; ext <= result.extensionCount; ext++) { + const base = ext * 128 + if (base + 128 > bytes.length) { + result.errors.push(`Extension block ${ext} declared but missing from file`) + break + } + if (bytes[base] === 0x02) { + try { + parseCeaBlock(bytes, base, result) + } catch { + result.errors.push(`Failed to parse CEA extension block ${ext}`) + } + } + } + + // Deduplicate timings (prefer detailed > cea-vic > standard > established). + const priority: Record = { detailed: 0, 'cea-vic': 1, standard: 2, established: 3 } + const seen = new Map() + for (const t of result.timings) { + const key = `${t.width}x${t.height}@${Math.round(t.refreshHz)}${t.interlaced ? 'i' : ''}` + const existing = seen.get(key) + if (!existing || priority[t.source] < priority[existing.source]) seen.set(key, t) + } + result.timings = Array.from(seen.values()).sort( + (a, b) => b.width * b.height - a.width * a.height || b.refreshHz - a.refreshHz + ) + + result.valid = result.errors.length === 0 || result.checksumOk + return result +} + +// --------------------------------------------------------------------------- +// monitor_profile.xml generation (IddCxMonitorConfig format) +// --------------------------------------------------------------------------- + +function fmt(n: number, decimals: number): string { + return n.toFixed(decimals) +} + +export function generateMonitorProfileXml(edid: ParsedEdid): string { + const lines: string[] = [] + lines.push(``) + lines.push(``) + lines.push(``) + lines.push(` `) + + const modes = edid.timings.filter((t) => !t.interlaced && t.refreshHz > 0) + for (const mode of modes) { + const nominal = Math.round(mode.refreshHz) + const isIntegral = Math.abs(mode.refreshHz - nominal) < 0.001 + lines.push(` `) + lines.push(` ${mode.width}`) + lines.push(` ${mode.height}`) + lines.push(` ${fmt(mode.refreshHz, 3)}`) + lines.push(` ${isIntegral ? 1000 : 999}`) + lines.push(` ${nominal}`) + lines.push(` `) + } + + lines.push(` `) + lines.push(` `) + lines.push(` sRGB`) + lines.push(` ${fmt(edid.gamma ?? 2.2, 3)}`) + const c: EdidChromaticity = + edid.chromaticity ?? { + redX: 0.64, + redY: 0.33, + greenX: 0.3, + greenY: 0.6, + blueX: 0.15, + blueY: 0.06, + whiteX: 0.3127, + whiteY: 0.329 + } + lines.push(` `) + lines.push(` ${fmt(c.redX, 4)}`) + lines.push(` ${fmt(c.redY, 4)}`) + lines.push(` ${fmt(c.greenX, 4)}`) + lines.push(` ${fmt(c.greenY, 4)}`) + lines.push(` ${fmt(c.blueX, 4)}`) + lines.push(` ${fmt(c.blueY, 4)}`) + lines.push(` ${fmt(c.whiteX, 4)}`) + lines.push(` ${fmt(c.whiteY, 4)}`) + lines.push(` `) + lines.push(` `) + + const preferred = edid.preferred ?? modes[0] + if (preferred) { + lines.push(` `) + lines.push(` ${preferred.width}`) + lines.push(` ${preferred.height}`) + lines.push(` ${fmt(preferred.refreshHz, 3)}`) + lines.push(` `) + } + + lines.push(``) + return lines.join('\n') +} diff --git a/VirtualDriverControl/src/shared/presets.ts b/VirtualDriverControl/src/shared/presets.ts new file mode 100644 index 00000000..b1c0c4cc --- /dev/null +++ b/VirtualDriverControl/src/shared/presets.ts @@ -0,0 +1,65 @@ +export interface ResolutionPreset { + width: number + height: number + label: string + category: 'Standard' | 'HD' | 'QHD' | '4K & Beyond' | 'Ultrawide' | 'Portable & Tablet' +} + +/** Curated subset of the upstream option.txt preset list (640x480 - 10240x4320). */ +export const RESOLUTION_PRESETS: ResolutionPreset[] = [ + { width: 640, height: 480, label: 'VGA', category: 'Standard' }, + { width: 800, height: 600, label: 'SVGA', category: 'Standard' }, + { width: 1024, height: 768, label: 'XGA', category: 'Standard' }, + { width: 1280, height: 1024, label: 'SXGA', category: 'Standard' }, + { width: 1400, height: 1050, label: 'SXGA+', category: 'Standard' }, + { width: 1600, height: 1200, label: 'UXGA', category: 'Standard' }, + + { width: 1280, height: 720, label: 'HD 720p', category: 'HD' }, + { width: 1366, height: 768, label: 'WXGA', category: 'HD' }, + { width: 1600, height: 900, label: 'HD+', category: 'HD' }, + { width: 1920, height: 1080, label: 'Full HD 1080p', category: 'HD' }, + { width: 1920, height: 1200, label: 'WUXGA', category: 'HD' }, + + { width: 2560, height: 1440, label: 'QHD 1440p', category: 'QHD' }, + { width: 2560, height: 1600, label: 'WQXGA', category: 'QHD' }, + { width: 2880, height: 1620, label: 'QHD+ 3K', category: 'QHD' }, + { width: 3200, height: 1800, label: 'WQXGA+', category: 'QHD' }, + + { width: 3840, height: 2160, label: '4K UHD', category: '4K & Beyond' }, + { width: 4096, height: 2160, label: 'DCI 4K', category: '4K & Beyond' }, + { width: 5120, height: 2880, label: '5K', category: '4K & Beyond' }, + { width: 6016, height: 3384, label: '6K', category: '4K & Beyond' }, + { width: 7680, height: 4320, label: '8K UHD', category: '4K & Beyond' }, + + { width: 2560, height: 1080, label: 'UW-FHD 21:9', category: 'Ultrawide' }, + { width: 3440, height: 1440, label: 'UW-QHD 21:9', category: 'Ultrawide' }, + { width: 3840, height: 1600, label: 'UW-QHD+ 24:10', category: 'Ultrawide' }, + { width: 5120, height: 1440, label: 'Super UW 32:9', category: 'Ultrawide' }, + { width: 5120, height: 2160, label: '5K2K 21:9', category: 'Ultrawide' }, + + { width: 1280, height: 800, label: 'WXGA Tablet', category: 'Portable & Tablet' }, + { width: 2048, height: 1536, label: 'iPad Retina 4:3', category: 'Portable & Tablet' }, + { width: 2160, height: 1440, label: 'Surface 3:2', category: 'Portable & Tablet' }, + { width: 2256, height: 1504, label: 'Surface Laptop 3:2', category: 'Portable & Tablet' }, + { width: 2736, height: 1824, label: 'Surface Pro 3:2', category: 'Portable & Tablet' }, + { width: 2880, height: 1920, label: 'Surface Pro 8+ 3:2', category: 'Portable & Tablet' } +] + +/** Common refresh-rate chips offered in the editor. */ +export const REFRESH_RATE_PRESETS: number[] = [24, 30, 50, 59.94, 60, 75, 90, 100, 120, 144, 165, 175, 200, 240, 360] + +export function aspectRatioLabel(width: number, height: number): string { + const gcd = (a: number, b: number): number => (b === 0 ? a : gcd(b, a % b)) + const g = gcd(width, height) + let w = width / g + let h = height / g + // Render familiar marketing ratios. + if (w === 8 && h === 5) [w, h] = [16, 10] + if (w === 7 && h === 3) [w, h] = [21, 9] + if (w === 64 && h === 27) [w, h] = [21, 9] + if (w === 43 && h === 18) [w, h] = [21, 9] + if (w === 12 && h === 5) [w, h] = [21, 9] + if (w === 32 && h === 10) [w, h] = [32, 10] + if (w > 40) return `${(width / height).toFixed(2)}:1` + return `${w}:${h}` +} diff --git a/VirtualDriverControl/src/shared/types.ts b/VirtualDriverControl/src/shared/types.ts new file mode 100644 index 00000000..59a1b052 --- /dev/null +++ b/VirtualDriverControl/src/shared/types.ts @@ -0,0 +1,419 @@ +/** + * Shared type contract between the main process, preload bridge and renderer. + */ + +// --------------------------------------------------------------------------- +// vdd_settings.xml model +// --------------------------------------------------------------------------- + +export type ColourFormat = 'RGB' | 'YCbCr444' | 'YCbCr422' | 'YCbCr420' + +export interface ResolutionEntry { + width: number + height: number + /** Per-resolution refresh rates (Hz). Fractional rates like 59.94 allowed. */ + refreshRates: number[] +} + +export interface VddSettings { + monitors: { count: number } + gpu: { friendlyName: string } + global: { refreshRates: number[] } + resolutions: ResolutionEntry[] + logging: { + sendLogsThroughPipe: boolean + logging: boolean + debugLogging: boolean + } + colour: { + sdr10bit: boolean + hdrPlus: boolean + colourFormat: ColourFormat + } + cursor: { + hardwareCursor: boolean + cursorMaxX: number + cursorMaxY: number + alphaCursorSupport: boolean + xorCursorSupportLevel: number + } + edid: { + customEdid: boolean + preventSpoof: boolean + edidCeaOverride: boolean + } + edidIntegration: { + enabled: boolean + autoConfigureFromEdid: boolean + edidProfilePath: string + overrideManualSettings: boolean + fallbackOnError: boolean + } + hdrAdvanced: { + hdr10StaticMetadata: { + enabled: boolean + maxDisplayMasteringLuminance: number + minDisplayMasteringLuminance: number + maxContentLightLevel: number + maxFrameAvgLightLevel: number + } + colorPrimaries: { + enabled: boolean + redX: number + redY: number + greenX: number + greenY: number + blueX: number + blueY: number + whiteX: number + whiteY: number + } + colorSpace: { + enabled: boolean + gammaCorrection: number + primaryColorSpace: string + enableMatrixTransform: boolean + } + } + autoResolutions: { + enabled: boolean + sourcePriority: string + edidModeFiltering: { + minRefreshRate: number + maxRefreshRate: number + excludeFractionalRates: boolean + minResolutionWidth: number + minResolutionHeight: number + maxResolutionWidth: number + maxResolutionHeight: number + } + preferredMode: { + useEdidPreferred: boolean + fallbackWidth: number + fallbackHeight: number + fallbackRefresh: number + } + } + colorAdvanced: { + bitDepthManagement: { + autoSelectFromColorSpace: boolean + forceBitDepth: number + fp16SurfaceSupport: boolean + } + colorFormatExtended: { + sdrWhiteLevel: number + } + } +} + +// --------------------------------------------------------------------------- +// Named pipe protocol +// --------------------------------------------------------------------------- + +/** Toggle commands understood by the driver pipe. */ +export type PipeToggleCommand = + | 'HDRPLUS' + | 'SDR10' + | 'CUSTOMEDID' + | 'PREVENTSPOOF' + | 'CEAOVERRIDE' + | 'HARDWARECURSOR' + | 'LOGGING' + | 'LOG_DEBUG' + +export interface PipeResult { + ok: boolean + command: string + /** Decoded response payload (log lines for most commands). */ + response: string + lines: string[] + durationMs: number + error?: string +} + +export interface DriverLiveSettings { + debug: boolean + log: boolean +} + +// --------------------------------------------------------------------------- +// Driver status +// --------------------------------------------------------------------------- + +export type DriverStatusLevel = + | 'online' // pipe answers PING + | 'installed-offline' // device or DLL present, pipe not answering + | 'not-installed' + | 'unknown' + +export interface DriverStatus { + level: DriverStatusLevel + pipeConnected: boolean + devicePresent: boolean + deviceName?: string + devicePnpStatus?: string + dllPresent: boolean + dllDate?: string + checkedAt: number +} + +export interface GpuInfo { + name: string + source: 'pipe' | 'wmi' + assigned: boolean + driverVersion?: string + vramMB?: number +} + +export interface SystemInfo { + windowsVersion: string + windowsBuild: string + arch: string + isAdmin: boolean + appVersion: string + electronVersion: string + settingsPath: string + logsDir: string +} + +// --------------------------------------------------------------------------- +// Logs +// --------------------------------------------------------------------------- + +export type LogSeverity = 'debug' | 'info' | 'warning' | 'error' +export type LogSource = 'file' | 'pipe' | 'app' + +export interface LogEvent { + id: number + timestamp: number + source: LogSource + severity: LogSeverity + message: string +} + +// --------------------------------------------------------------------------- +// Settings persistence +// --------------------------------------------------------------------------- + +export interface BackupInfo { + fileName: string + fullPath: string + createdAt: number + sizeBytes: number +} + +export interface SettingsLoadResult { + ok: boolean + settings?: VddSettings + rawXml?: string + /** True when the file did not exist and defaults were returned. */ + isDefault: boolean + error?: string +} + +export interface SaveResult { + ok: boolean + backupCreated?: string + error?: string +} + +// --------------------------------------------------------------------------- +// EDID +// --------------------------------------------------------------------------- + +export interface EdidTiming { + width: number + height: number + refreshHz: number + interlaced?: boolean + source: 'detailed' | 'standard' | 'established' | 'cea-vic' + pixelClockMHz?: number + vic?: number + native?: boolean +} + +export interface EdidChromaticity { + redX: number + redY: number + greenX: number + greenY: number + blueX: number + blueY: number + whiteX: number + whiteY: number +} + +export interface EdidHdrMetadata { + eotfSdr: boolean + eotfHdr: boolean + eotfPq: boolean + eotfHlg: boolean + maxLuminance?: number + maxFrameAvgLuminance?: number + minLuminance?: number +} + +export interface ParsedEdid { + valid: boolean + errors: string[] + manufacturerId: string + productCode: number + serialNumber: number + serialString?: string + displayName?: string + manufactureWeek: number + manufactureYear: number + edidVersion: string + digital: boolean + bitDepth?: number + videoInterface?: string + screenWidthCm?: number + screenHeightCm?: number + gamma?: number + chromaticity?: EdidChromaticity + timings: EdidTiming[] + preferred?: EdidTiming + extensionCount: number + hasCeaExtension: boolean + ceaYcbcr444?: boolean + ceaYcbcr422?: boolean + ceaBasicAudio?: boolean + hdr?: EdidHdrMetadata + checksumOk: boolean + rawBytes: number +} + +// --------------------------------------------------------------------------- +// Driver installer (GitHub releases) +// --------------------------------------------------------------------------- + +/** Drivers managed by the lifecycle installer. */ +export type ManagedDriverId = 'display' | 'audio' + +export interface ManagedDeviceState { + /** Number of root-enumerated device nodes present. */ + count: number + /** PnP status of each device (OK, Error, ...). */ + statuses: string[] +} + +export interface ReleaseInfo { + tag: string + name: string + publishedAt: string + notes: string + htmlUrl: string + asset: { + name: string + sizeBytes: number + downloadUrl: string + sha256?: string + } | null +} + +export type InstallPhase = 'download' | 'verify' | 'extract' | 'install' | 'finalize' + +export interface InstallProgress { + phase: InstallPhase + /** 0-100 within the current phase; -1 = indeterminate. */ + percent: number + message: string +} + +export interface LifecycleResult { + ok: boolean + /** Tail of the elevated operation's log. */ + detail?: string + error?: string +} + +// --------------------------------------------------------------------------- +// Windows audio endpoints +// --------------------------------------------------------------------------- + +export type AudioFlow = 'render' | 'capture' + +export interface AudioEndpoint { + /** MMDevice endpoint ID, e.g. {0.0.0.00000000}.{guid}. */ + id: string + name: string + flow: AudioFlow + isDefault: boolean + isDefaultComm: boolean + /** Master volume scalar 0..1. */ + volume: number + muted: boolean + /** True when the endpoint belongs to the Virtual Audio Driver. */ + isVirtual: boolean +} + +// --------------------------------------------------------------------------- +// Audio routing (renderer-side WebAudio pump, persisted in prefs) +// --------------------------------------------------------------------------- + +/** Source id for routing system output audio (WASAPI loopback of default output). */ +export const SYSTEM_AUDIO_SOURCE = 'system-loopback' + +export interface AudioRoute { + id: string + /** Web `MediaDeviceInfo.deviceId` of an audioinput, or SYSTEM_AUDIO_SOURCE. */ + sourceId: string + sourceLabel: string + /** Web `MediaDeviceInfo.deviceId` of an audiooutput. */ + sinkId: string + sinkLabel: string + /** Gain 0..2 (1 = unity). */ + gain: number + enabled: boolean +} + +// --------------------------------------------------------------------------- +// Display layout (physical + virtual monitors) +// --------------------------------------------------------------------------- + +export interface DisplayLayoutInfo { + id: number + label: string + bounds: { x: number; y: number; width: number; height: number } + workArea: { x: number; y: number; width: number; height: number } + scaleFactor: number + rotation: number + frequency: number + internal: boolean + primary: boolean + /** True when the monitor hangs off the MttVDD virtual adapter. */ + isVirtual: boolean + colorDepth: number +} + +// --------------------------------------------------------------------------- +// App preferences (renderer-local, persisted via main) +// --------------------------------------------------------------------------- + +export interface AppPreferences { + theme: 'dark' | 'light' | 'system' + accent: string + baseDir: string + /** Saved audio routes, re-armed on app start. */ + audioRoutes: AudioRoute[] +} + +export const DEFAULT_BASE_DIR = 'C:\\VirtualDisplayDriver' + +/** Result of changing the driver folder (prefs + VDDPATH registry together). */ +export interface BaseDirResult { + ok: boolean + prefs: AppPreferences + error?: string +} + +// --------------------------------------------------------------------------- +// IPC event channel payloads (main -> renderer push) +// --------------------------------------------------------------------------- + +export interface PipeActivity { + command: string + ok: boolean + durationMs: number + at: number +} diff --git a/VirtualDriverControl/tsconfig.json b/VirtualDriverControl/tsconfig.json new file mode 100644 index 00000000..155ebaa6 --- /dev/null +++ b/VirtualDriverControl/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.web.json" } + ] +} diff --git a/VirtualDriverControl/tsconfig.node.json b/VirtualDriverControl/tsconfig.node.json new file mode 100644 index 00000000..b603c8f5 --- /dev/null +++ b/VirtualDriverControl/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "composite": true, + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2023"], + "types": ["node"], + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "noEmit": true, + "paths": { + "@shared/*": ["./src/shared/*"] + } + }, + "include": [ + "electron.vite.config.ts", + "src/main/**/*", + "src/preload/**/*", + "src/shared/**/*" + ] +} diff --git a/VirtualDriverControl/tsconfig.web.json b/VirtualDriverControl/tsconfig.web.json new file mode 100644 index 00000000..1b3224fa --- /dev/null +++ b/VirtualDriverControl/tsconfig.web.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "composite": true, + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "useDefineForClassFields": true, + "noEmit": true, + "paths": { + "@renderer/*": ["./src/renderer/src/*"], + "@shared/*": ["./src/shared/*"] + } + }, + "include": [ + "src/renderer/src/**/*", + "src/preload/index.d.ts", + "src/shared/**/*" + ] +} From 0f54bc4ec01760592c90018d07523f8180f7354d Mon Sep 17 00:00:00 2001 From: Mike Rodriguez Date: Sat, 19 Sep 2026 04:02:25 -0700 Subject: [PATCH 07/10] fix: preserve four-part control panel versions --- .github/workflows/ci-validation.yml | 13 +++++++++++-- VirtualDriverControl/electron.vite.config.ts | 3 +++ VirtualDriverControl/package.json | 2 +- .../src/main/services/driver-service.ts | 2 +- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-validation.yml b/.github/workflows/ci-validation.yml index 5071a279..27fb70a6 100644 --- a/.github/workflows/ci-validation.yml +++ b/.github/workflows/ci-validation.yml @@ -62,8 +62,11 @@ jobs: throw "GITHUB_RUN_NUMBER must fit an INF version component (0-65535): $buildNumber" } - $releaseVersion = "{0}.{1}.{2}.{3}" -f ($now.Year % 100), $now.Month, $now.Day, $buildNumber + $releaseBaseVersion = "{0}.{1}.{2}" -f ($now.Year % 100), $now.Month, $now.Day + $releaseVersion = "$releaseBaseVersion.$buildNumber" Write-Output "Using release version $releaseVersion" + "RELEASE_BASE_VERSION=$releaseBaseVersion" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "RELEASE_BUILD_NUMBER=$buildNumber" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append "RELEASE_VERSION=$releaseVersion" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - name: Setup MSBuild @@ -191,9 +194,15 @@ jobs: npm audit --audit-level=moderate if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - npm version $env:RELEASE_VERSION --no-git-tag-version + # npm package versions are three-part SemVer. electron-builder + # combines this base with BUILD_NUMBER for the Windows four-part + # FileVersion and ${buildVersion} artifact-name macro. + npm version $env:RELEASE_BASE_VERSION --no-git-tag-version if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + $env:BUILD_NUMBER = $env:RELEASE_BUILD_NUMBER + $env:VDC_RELEASE_VERSION = $env:RELEASE_VERSION + npm run typecheck if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } diff --git a/VirtualDriverControl/electron.vite.config.ts b/VirtualDriverControl/electron.vite.config.ts index efe70543..fc8b85b2 100644 --- a/VirtualDriverControl/electron.vite.config.ts +++ b/VirtualDriverControl/electron.vite.config.ts @@ -4,6 +4,9 @@ import { resolve } from 'path' export default defineConfig({ main: { + define: { + 'process.env.VDC_RELEASE_VERSION': JSON.stringify(process.env.VDC_RELEASE_VERSION ?? '') + }, resolve: { alias: { '@shared': resolve(__dirname, 'src/shared') diff --git a/VirtualDriverControl/package.json b/VirtualDriverControl/package.json index 14000433..91018a13 100644 --- a/VirtualDriverControl/package.json +++ b/VirtualDriverControl/package.json @@ -40,7 +40,7 @@ "win": { "icon": "./resources/VirtualDisplayDriver.ico", "target": "portable", - "artifactName": "${productName}-${version}-${arch}.${ext}" + "artifactName": "${productName}-${buildVersion}-${arch}.${ext}" }, "portable": { "requestExecutionLevel": "admin" diff --git a/VirtualDriverControl/src/main/services/driver-service.ts b/VirtualDriverControl/src/main/services/driver-service.ts index b36f0a1e..5151639a 100644 --- a/VirtualDriverControl/src/main/services/driver-service.ts +++ b/VirtualDriverControl/src/main/services/driver-service.ts @@ -131,7 +131,7 @@ export class DriverService { windowsBuild: os.release(), arch: os.arch(), isAdmin, - appVersion: app.getVersion(), + appVersion: process.env.VDC_RELEASE_VERSION || app.getVersion(), electronVersion: process.versions.electron, settingsPath: `${this.paths.getBaseDir()}\\vdd_settings.xml`, logsDir: `${this.paths.getBaseDir()}\\Logs` From e233e5ea886cdb69178e849a49a084fc5ab2bafe Mon Sep 17 00:00:00 2001 From: Mike Rodriguez Date: Sat, 19 Sep 2026 04:19:30 -0700 Subject: [PATCH 08/10] ci: work around SignPath v3 artifact access regression --- .github/workflows/ci-validation.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-validation.yml b/.github/workflows/ci-validation.yml index 27fb70a6..3b4d3d98 100644 --- a/.github/workflows/ci-validation.yml +++ b/.github/workflows/ci-validation.yml @@ -288,7 +288,10 @@ jobs: - name: Submit VDD package to SignPath id: submit_signing if: env.SIGNPATH_SIGNING_RUN == 'true' - uses: signpath/github-action-submit-signing-request@f6d04783b4569d051e0c80105fe66e82819d0092 # v3.0.0 + # v3.0.0 currently rejects otherwise valid GitHub artifacts with + # "User has no access to the requested GitHub resource" (upstream #18). + # v2.3 is the last Node 24-compatible release before that regression. + uses: signpath/github-action-submit-signing-request@c92b958760219087e01f8d67a1669ed57afe2627 # v2.3.0 with: api-token: ${{ secrets.SIGNPATH_API_TOKEN }} organization-id: ${{ vars.SIGNPATH_ORG_ID }} From 43c5e4d917b85c7769f7d01ed08c4d2d581609a5 Mon Sep 17 00:00:00 2001 From: Mike Rodriguez Date: Sat, 19 Sep 2026 04:28:11 -0700 Subject: [PATCH 09/10] ci: fix signed package job summary --- .github/workflows/ci-validation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-validation.yml b/.github/workflows/ci-validation.yml index 3b4d3d98..10efb56a 100644 --- a/.github/workflows/ci-validation.yml +++ b/.github/workflows/ci-validation.yml @@ -361,5 +361,5 @@ jobs: SIGNING_REQUEST_URL: ${{ steps.submit_signing.outputs.signing-request-web-url }} run: | "## SignPath signing (${{ matrix.platform }})" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append - "- Signed package: `VDD-${{ matrix.platform }}-${{ env.BUILD_CONFIGURATION }}-signed`" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append + '- Signed package: `VDD-${{ matrix.platform }}-${{ env.BUILD_CONFIGURATION }}-signed`' | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append "- Signing request: $env:SIGNING_REQUEST_URL" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append From 68043d91ef65b2707f01db50749bf02cdd41b1e3 Mon Sep 17 00:00:00 2001 From: Mike Rodriguez Date: Sat, 19 Sep 2026 04:36:22 -0700 Subject: [PATCH 10/10] ci: pin released UMDF baseline --- .github/workflows/ci-validation.yml | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci-validation.yml b/.github/workflows/ci-validation.yml index 10efb56a..3b153e5a 100644 --- a/.github/workflows/ci-validation.yml +++ b/.github/workflows/ci-validation.yml @@ -28,6 +28,10 @@ env: BUILD_CONFIGURATION: Release VDD_SOLUTION: Virtual Display Driver (HDR)/MttVDD.sln CONTROL_PANEL_DIR: VirtualDriverControl + # Keep the project's Windows 10-compatible released UMDF baseline. Never + # select the highest directory from the runner: WDKs can contain preview WDF + # headers (for example 2.35) that retail Windows rejects at load time. + UMDF_VERSION: '2.25' # Signing is deliberate: release tags sign automatically, and a manual run # must explicitly opt in. Pull requests and ordinary branch pushes never # receive the SignPath token. @@ -125,7 +129,7 @@ jobs: $sln = "${{ env.VDD_SOLUTION }}" if (-not (Test-Path $sln)) { throw "VDD solution file not found at: $sln" } - # Pick an available UMDF WDF header version on the runner. + # Resolve the project's explicitly supported UMDF WDF headers. # Kits can be laid out either versioned: # Include\\wdf\umdf\2.xx # or unversioned: @@ -139,17 +143,17 @@ jobs: throw "UMDF WDF include root not found under $env:WINDOWS_SDK_DIR (tried versioned + unversioned layouts)" } - $umdfBest = + $umdfDirectory = $umdfRoots | - ForEach-Object { Get-ChildItem -Path $_ -Directory -ErrorAction SilentlyContinue } | - Where-Object { $_.Name -match '^\d+\.\d+$' } | - Sort-Object -Property Name -Descending | + ForEach-Object { Get-ChildItem -Path $_ -Directory -Filter $env:UMDF_VERSION -ErrorAction SilentlyContinue } | Select-Object -First 1 - if (-not $umdfBest) { throw "No UMDF version directories found under: $($umdfRoots -join ', ')" } + if (-not $umdfDirectory) { + throw "Released UMDF $env:UMDF_VERSION headers were not found under: $($umdfRoots -join ', ')" + } - $umdfMinor = ($umdfBest.Name -split '\.')[1] - Write-Output "Using UMDF version: $($umdfBest.Name) (minor=$umdfMinor) from $($umdfBest.FullName)" + $umdfMinor = ($env:UMDF_VERSION -split '\.')[1] + Write-Output "Using pinned UMDF version: $env:UMDF_VERSION (minor=$umdfMinor) from $($umdfDirectory.FullName)" $now = [DateTime]::UtcNow $driverVersion = $env:RELEASE_VERSION @@ -256,6 +260,11 @@ jobs: } } + $inf = Join-Path $dest "MttVDD.inf" + if ((Select-String -Path $inf -Pattern "^UmdfLibraryVersion=$([regex]::Escape($env:UMDF_VERSION))\.0\s*$").Count -ne 1) { + throw "Built INF does not target the required released UMDF $env:UMDF_VERSION baseline" + } + - name: Upload unsigned driver package id: upload_unsigned_driver_package uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1